2804 Fixed problem with link of Sales person on ticket descriptor #558

Merged
jgallego merged 2 commits from 2804-ticket_descriptor_salesPerson into dev 2021-02-25 15:12:20 +00:00
29 changed files with 650 additions and 32628 deletions
Showing only changes of commit 0d7a41f862 - Show all commits

View File

@ -1,11 +1,51 @@
const fs = require('fs-extra');
const sharp = require('sharp');
const path = require('path');
const readChunk = require('read-chunk');
const imageType = require('image-type');
const bmp = require('bmp-js');
module.exports = Self => {
require('../methods/image/download')(Self);
require('../methods/image/upload')(Self);
// Function extracted from jimp package (utils)
function scan(image, x, y, w, h, f) {
// round input
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
for (let _y = y; _y < y + h; _y++) {
for (let _x = x; _x < x + w; _x++) {
const idx = (image.bitmap.width * _y + _x) << 2;
f.call(image, _x, _y, idx);
}
}
return image;
}
// Function extracted from jimp package (type-bmp)
function fromAGBR(bitmap) {
return scan({bitmap}, 0, 0, bitmap.width, bitmap.height, function(
x,
y,
index
) {
const alpha = this.bitmap.data[index + 0];
const blue = this.bitmap.data[index + 1];
const green = this.bitmap.data[index + 2];
const red = this.bitmap.data[index + 3];
this.bitmap.data[index + 0] = red;
this.bitmap.data[index + 1] = green;
this.bitmap.data[index + 2] = blue;
this.bitmap.data[index + 3] = bitmap.is_with_alpha ? alpha : 0xff;
}).bitmap;
}
Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
@ -48,13 +88,31 @@ module.exports = Self => {
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
const buffer = readChunk.sync(srcFilePath, 0, 12);
const type = imageType(buffer);
let sharpOptions;
let imgSrc = srcFilePath;
if (type.mime == 'image/bmp') {
const bmpBuffer = fs.readFileSync(srcFilePath);
const bmpData = fromAGBR(bmp.decode(bmpBuffer));
imgSrc = bmpData.data;
sharpOptions = {
raw: {
width: bmpData.width,
height: bmpData.height,
channels: 4
}
};
}
const resizeOpts = {
withoutEnlargement: true,
fit: 'inside'
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
.png()
.toFile(dstFile);
@ -69,7 +127,7 @@ module.exports = Self => {
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(size.width, size.height, resizeOpts)
.png()
.toFile(dstFile);

View File

@ -174,4 +174,9 @@ vn-table {
.vn-check {
margin: 0;
}
.empty-rows {
color: $color-font-secondary;
font-size: 1.375rem;
text-align: center;
}
}

View File

@ -26,15 +26,25 @@ module.exports = Self => {
Self.lastActiveTickets = async(id, ticketId) => {
const ticket = await Self.app.models.Ticket.findById(ticketId);
const query = `
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName, ad.city AS address
FROM vn.ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id
JOIN vn.address ad ON t.addressFk = ad.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0
AND t.id <> ? AND t.warehouseFk = ?
ORDER BY t.shipped
SELECT
t.id,
t.shipped,
a.name AS agencyName,
w.name AS warehouseName,
ad.nickname AS nickname,
ad.city AS city,
ad.postalCode AS postalCode,
ad.street AS street,
pr.name AS name
FROM ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id
JOIN vn.address ad ON t.addressFk = ad.id
JOIN vn.province pr ON ad.provinceFk = pr.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0
AND t.id <> ? AND t.warehouseFk = ?
ORDER BY t.shipped
LIMIT 10`;
return Self.rawSql(query, [id, ticketId, ticket.warehouseFk]);

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('Client last active tickets', () => {
it('should receive an array of last active tickets of Bruce Wayne', async() => {
const ticketId = 22;
const clientId = 109;
const warehouseId = 5;
const result = await app.models.Client.lastActiveTickets(clientId, ticketId, warehouseId);
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
const properties = Object.keys(anyResult);
expect(properties.length).toEqual(9);
expect(result.length).toEqual(3);
});
});

View File

@ -78,6 +78,8 @@ module.exports = Self => {
return {'ic.id': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'code':
return {'it.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'active':
@ -103,6 +105,7 @@ module.exports = Self => {
i.id AS itemFk,
i.size,
i.density,
it.code,
i.typeFk,
i.family,
i.isActive,

View File

@ -37,8 +37,8 @@
<vn-th field="quantity">Quantity</vn-th>
<vn-th field="description" style="text-align: center">Description</vn-th>
<vn-th field="size">Size</vn-th>
<vn-th field="tags" style="text-align: center">Tags</vn-th>
<vn-th field="type">Type</vn-th>
<vn-th field="name" style="text-align: center">Tags</vn-th>
<vn-th field="code">Type</vn-th>
<vn-th field="intrastat">Intrastat</vn-th>
<vn-th field="origin">Origin</vn-th>
<vn-th field="density">Density</vn-th>
@ -109,7 +109,7 @@
</vn-fetched-tags>
</vn-td>
<vn-td shrink title="{{::buy.type}}">
{{::buy.type}}
{{::buy.code}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::buy.intrastat}}

View File

@ -46,13 +46,8 @@ module.exports = Self => {
if (!image) return;
const srcFile = image.url.split('/').pop();
const dotIndex = srcFile.lastIndexOf('.');
let fileName = srcFile.substring(0, dotIndex);
if (dotIndex == -1)
fileName = srcFile;
const srcFile = image.url;
const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, '');
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);

View File

@ -44,6 +44,10 @@ module.exports = Self => {
arg: 'description',
type: 'String',
description: 'The item description',
}, {
arg: 'stemMultiplier',
type: 'Integer',
description: 'The item multiplier',
}
],
returns: {
@ -80,16 +84,22 @@ module.exports = Self => {
: {or: [{'i.name': {like: `%${value}%`}}, codeWhere]};
case 'id':
return {'i.id': value};
case 'description':
return {'i.description': {like: `%${value}%`}};
case 'categoryFk':
return {'ic.id': value};
case 'salesPersonFk':
return {'t.workerFk': value};
case 'typeFk':
return {'i.typeFk': value};
case 'isActive':
return {'i.isActive': value};
case 'multiplier':
return {'i.stemMultiplier': value};
case 'typeFk':
return {'i.typeFk': value};
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'intrastat':
return {'intr.description': value};
}
});
filter = mergeFilters(filter, {where});
@ -98,7 +108,8 @@ module.exports = Self => {
let stmt;
stmt = new ParameterizedSQL(
`SELECT i.id,
`SELECT
i.id,
i.image,
i.name,
i.description,
@ -111,29 +122,30 @@ module.exports = Self => {
i.tag10, i.value10,
i.subName,
i.isActive,
t.name type,
t.workerFk buyerFk,
u.name userName,
intr.description AS intrastat,
i.stems,
ori.code AS origin,
ic.name AS category,
i.density,
i.stemMultiplier,
i.typeFk,
it.name AS typeName,
it.workerFk AS buyerFk,
u.name AS userName,
ori.code AS origin,
ic.name AS category,
intr.description AS intrastat,
b.grouping,
b.packing,
itn.code AS niche, @visibleCalc
ip.code AS niche, @visibleCalc
FROM item i
LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = t.categoryFk
LEFT JOIN worker w ON w.id = t.workerFk
LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN worker w ON w.id = it.workerFk
LEFT JOIN account.user u ON u.id = w.userFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN producer pr ON pr.id = i.producerFk
LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = t.warehouseFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = it.warehouseFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
LEFT JOIN itemPlacement itn ON itn.itemFk = i.id AND itn.warehouseFk = t.warehouseFk`
LEFT JOIN itemPlacement ip ON ip.itemFk = i.id AND ip.warehouseFk = it.warehouseFk`
);
if (ctx.args.tags) {

View File

@ -113,6 +113,12 @@
ng-model="$ctrl.item.stems"
rule>
</vn-input-number>
<vn-input-number
vn-one
min="0"
label="Multiplier"
ng-model="$ctrl.item.stemMultiplier">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number

View File

@ -10,3 +10,4 @@ New intrastat: Nuevo intrastat
Identifier: Identificador
Fragile: Frágil
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Multiplier: Multiplicador

View File

@ -5,107 +5,107 @@
model="model"
class="vn-w-xl vn-mb-xl">
<vn-card>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="type" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.type}}">
{{::item.type}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="name">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="typeFk" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.typeName}}">
{{::item.typeName}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
@ -128,3 +128,30 @@
item="$ctrl.itemSelected">
</vn-item-summary>
</vn-popup>
<vn-contextmenu
vn-id="contextmenu"
targets="['vn-data-viewer']"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -11,6 +11,36 @@ class Controller extends Section {
};
}
exprBuilder(param, value) {
switch (param) {
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'grouping':
return {'b.grouping': value};
case 'packing':
return {'b.packing': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'intrastat':
return {'intr.description': value};
case 'id':
case 'size':
case 'name':
case 'subname':
case 'isActive':
case 'density':
case 'stemMultiplier':
case 'stems':
return {[`i.${param}`]: value};
}
}
onCloneAccept(itemFk) {
return this.$http.post(`Items/${itemFk}/clone`)
.then(res => {

View File

@ -1,6 +1,6 @@
<div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
@ -8,7 +8,7 @@
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Ticket id"
@ -25,7 +25,7 @@
<tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Client id"
@ -38,19 +38,37 @@
url="Warehouses">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
ng-model="filter.from"
on-change="$ctrl.from = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
ng-model="filter.to"
on-change="$ctrl.to = value">
</vn-date-picker>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</vn-input-number>
<vn-icon color-marginal
icon="info"
vn-tooltip="Cannot choose a range of dates and days onward at the same time">
</vn-icon>
</vn-horizontal>
<vn-horizontal>
</section>
<vn-horizontal class="vn-px-lg">
<vn-check vn-one
triple-state="true"
label="For me"
@ -65,7 +83,7 @@
<tpl-item>{{name}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>

View File

@ -11,6 +11,35 @@ class Controller extends SearchPanel {
{code: 'denied', name: this.$t('Denied')}
];
}
get from() {
return this._from;
}
set from(value) {
this._from = value;
this.filter.scopeDays = null;
}
get to() {
return this._to;
}
set to(value) {
this._to = value;
this.filter.scopeDays = null;
}
get scopeDays() {
return this._scopeDays;
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.from = null;
this.filter.to = null;
}
}
ngModule.vnComponent('vnRequestSearchPanel', {

View File

@ -0,0 +1,48 @@
import './index';
describe(' Component vnRequestSearchPanel', () => {
let controller;
beforeEach(ngModule('item'));
beforeEach(inject($componentController => {
controller = $componentController('vnRequestSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('from() setter', () => {
it('should clear the scope days when setting the from property', () => {
controller.filter.scopeDays = 1;
controller.from = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.from).toBeDefined();
});
});
describe('to() setter', () => {
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.to = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.to).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.from = new Date();
controller.filter.to = new Date();
controller.scopeDays = 1;
expect(controller.filter.from).toBeNull();
expect(controller.filter.to).toBeNull();
expect(controller.scopeDays).toBeDefined();
});
});
});

View File

@ -10,9 +10,10 @@
<vn-portal slot="topbar">
<vn-searchbar
panel="vn-request-search-panel"
suggested-filter="$ctrl.filterParams"
info="Search request by id or alias"
suggested-filter="$ctrl.filterParams"
filter="$ctrl.filterParams"
fetch-params="$ctrl.fetchParams($params)"
model="model"
auto-state="false">
</vn-searchbar>

View File

@ -15,7 +15,6 @@ export default class Controller extends Section {
nextWeek.setDate(nextWeek.getDate() + 7);
this.filterParams = {
mine: true,
from: today,
to: nextWeek,
state: 'pending'
@ -23,6 +22,24 @@ export default class Controller extends Section {
}
}
fetchParams($params) {
if (!Object.entries($params).length)
$params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') {
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + $params.scopeDays);
to.setHours(23, 59, 59, 999);
Object.assign($params, {from, to});
}
return $params;
}
getState(isOk) {
if (isOk === null)
return 'Pending';

View File

@ -55,6 +55,9 @@
<vn-label-value label="stems"
value="{{$ctrl.summary.item.stems}}">
</vn-label-value>
<vn-label-value label="Multiplier"
value="{{$ctrl.summary.item.stemMultiplier}}">
</vn-label-value>
<vn-label-value label="Buyer">
<span
ng-click="workerDescriptor.show($event, $ctrl.summary.item.itemType.worker.userFk)"

View File

@ -326,31 +326,49 @@
icon="info">
</vn-icon>
</vn-horizontal>
<vn-table class="destinationTable">
<vn-thead>
<vn-tr>
<vn-th number>Id</vn-th>
<vn-th number>Shipped</vn-th>
<vn-th number>Agency</vn-th>
<vn-th number>Warehouse</vn-th>
<vn-th number>Address</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-data-viewer data="$ctrl.transfer.lastActiveTickets">
</vn-data-viewer>
<vn-tr
<table class="destinationTable vn-table">
<thead>
<tr>
<th translate shrink>Id</th>
<th translate>Shipped</th>
<th translate shrink>Agency</th>
<th translate expand>Address</th>
</tr>
</thead>
<tbody>
<tr
class="clickable"
ng-repeat="ticket in $ctrl.transfer.lastActiveTickets track by ticket.id"
ng-click="$ctrl.transferSales(ticket.id)">
<vn-td number>{{::ticket.id}}</vn-td>
<vn-td number>{{::ticket.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::ticket.agencyName}}</vn-td>
<vn-td number>{{::ticket.warehouseName}}</vn-td>
<vn-td number>{{::ticket.address}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<td shrink>{{::ticket.id}}</td>
<td>{{::ticket.shipped | date: 'dd/MM/yyyy'}}</td>
<td shrink>{{::ticket.agencyName}}</td>
<td expand>{{::ticket.address}}
<span vn-tooltip="
{{::ticket.nickname}}
{{::ticket.name}}
{{::ticket.street}}
{{::ticket.postalCode}}
{{::ticket.city}}">
{{::ticket.nickname}}
{{::ticket.name}}
{{::ticket.street}}
{{::ticket.postalCode}}
{{::ticket.city}}
</span>
</td>
</tr>
<tr>
<td
ng-if="!$ctrl.transfer.lastActiveTickets.length"
class="empty-rows"
colspan="4"
translate>
No results
</td>
</tr>
</tbody>
</table>
<form name="form">
<vn-horizontal class="vn-py-md">
<vn-input-number vn-one

View File

@ -1,3 +1,4 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('getAverageDays', {
description: 'Returns the average days duration and the two warehouses of the travel.',
@ -8,7 +9,7 @@ module.exports = Self => {
required: true
}],
returns: {
type: 'number',
type: 'object',
root: true
},
http: {
@ -18,15 +19,48 @@ module.exports = Self => {
});
Self.getAverageDays = async agencyModeFk => {
const query = `
SELECT t.id, t.warehouseInFk, t.warehouseOutFk,
(SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped )))
FROM travel t
WHERE t.agencyFk = ? LIMIT 50) AS dayDuration
FROM travel t
WHERE t.agencyFk = ? ORDER BY t.id DESC LIMIT 1;`;
const conn = Self.dataSource.connector;
let stmts = [];
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.travel');
stmt = new ParameterizedSQL(`
CREATE TEMPORARY TABLE tmp.travel (
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
t.landed,
t.shipped,
t.agencyFk
FROM travel t
WHERE t.agencyFk = ? LIMIT 50)`, [agencyModeFk]);
stmts.push(stmt);
stmt = new ParameterizedSQL(`
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
(SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped )))
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY id DESC LIMIT 50) AS dayDuration
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY t.id DESC LIMIT 1`);
const avgDaysIndex = stmts.push(stmt) - 1;
stmts.push(
`DROP TEMPORARY TABLE
tmp.travel`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const [avgDays] = result[avgDaysIndex];
const [avgDays] = await Self.rawSql(query, [agencyModeFk, agencyModeFk]);
return avgDays;
};
};

View File

@ -22,6 +22,9 @@ class Controller extends Section {
agencyModeFk: this.travel.agencyModeFk
};
this.$http.get(query, {params}).then(res => {
if (!res.data)
return;
const landed = new Date(value);
const futureDate = landed.getDate() + res.data.dayDuration;
landed.setDate(futureDate);

View File

@ -51,14 +51,29 @@ describe('Travel Component vnTravelCreate', () => {
expect(controller.travel.warehouseOutFk).toBeUndefined();
});
it(`should do nothing if there's no response data.`, () => {
controller.travel = {agencyModeFk: 4};
const tomorrow = new Date();
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;
$httpBackend.expectGET(query).respond(undefined);
controller.onShippedChange(tomorrow);
$httpBackend.flush();
expect(controller.travel.warehouseInFk).toBeUndefined();
expect(controller.travel.warehouseOutFk).toBeUndefined();
expect(controller.travel.dayDuration).toBeUndefined();
});
it(`should fill the fields when it's selected a date and agency.`, () => {
controller.travel = {agencyModeFk: 1};
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setDate(tomorrow.getDate() + 9);
const expectedResponse = {
dayDuration: 2,
warehouseInFk: 1,
warehouseOutFk: 2
id: 8,
dayDuration: 9,
warehouseInFk: 5,
warehouseOutFk: 1
};
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;

View File

@ -42,6 +42,11 @@
"model": "Account",
"foreignKey": "userFk"
},
"boss": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "bossFk"
},
"client": {
"type": "belongsTo",
"model": "Client",

View File

@ -29,6 +29,15 @@
ng-model="$ctrl.worker.phone"
rule>
</vn-textfield>
<vn-autocomplete
disabled="false"
ng-model="$ctrl.worker.bossFk"
url="Clients/activeWorkersWithRole"
show-field="nickname"
search-function="{firstName: $search}"
where="{role: 'employee'}"
label="Boss">
</vn-autocomplete>
</vn-horizontal>
</vn-vertical>
</vn-card>

View File

@ -7,6 +7,7 @@ Extension: Extensión
Fiscal identifier: NIF
Go to client: Ir al cliente
Last name: Apellidos
Boss: Jefe
Log: Historial
Private Branch Exchange: Centralita
Role: Rol

View File

@ -30,6 +30,14 @@
<vn-label-value label="Department"
value="{{worker.department.department.name}}">
</vn-label-value>
<vn-label-value
label="Boss">
<span
ng-click="workerDescriptor.show($event, worker.boss.id)"
class="link">
{{::worker.boss.nickname}}
</span>
</vn-label-value>
<vn-label-value label="Phone"
value="{{worker.phone}}">
</vn-label-value>
@ -51,3 +59,6 @@
</vn-one>
</vn-horizontal>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -11,8 +11,8 @@ class Controller extends Summary {
this.$.worker = null;
if (!value) return;
let query = `Workers/${value.id}`;
let filter = {
const query = `Workers/${value.id}`;
const filter = {
include: [
{
relation: 'user',
@ -31,13 +31,20 @@ class Controller extends Summary {
}
}]
}
}, {
},
{
relation: 'client',
scope: {fields: ['fi']}
}, {
},
{
relation: 'boss',
scope: {fields: ['id', 'nickname']}
},
{
relation: 'sip',
scope: {fields: ['extension']}
}, {
},
{
relation: 'department',
scope: {
include: {

32497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,12 @@
"node": ">=12"
},
"dependencies": {
"bmp-js": "^0.1.0",
"compression": "^1.7.3",
"fs-extra": "^5.0.0",
"helmet": "^3.21.2",
"i18n": "^0.8.4",
"image-type": "^4.1.0",
"imap": "^0.8.19",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
@ -30,6 +32,7 @@
"node-ssh": "^11.0.0",
"object-diff": "0.0.4",
"object.pick": "^1.3.0",
"read-chunk": "^3.2.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.8",
"require-yaml": "0.0.1",