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 fs = require('fs-extra');
const sharp = require('sharp'); const sharp = require('sharp');
const path = require('path'); const path = require('path');
const readChunk = require('read-chunk');
const imageType = require('image-type');
const bmp = require('bmp-js');
module.exports = Self => { module.exports = Self => {
require('../methods/image/download')(Self); require('../methods/image/download')(Self);
require('../methods/image/upload')(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) => { Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({}); const tx = await Self.beginTransaction({});
@ -48,13 +88,31 @@ module.exports = Self => {
const dstDir = path.join(collectionDir, 'full'); const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file); 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 = { const resizeOpts = {
withoutEnlargement: true, withoutEnlargement: true,
fit: 'inside' fit: 'inside'
}; };
await fs.mkdir(dstDir, {recursive: true}); await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false}) await sharp(imgSrc, sharpOptions)
.resize(collection.maxWidth, collection.maxHeight, resizeOpts) .resize(collection.maxWidth, collection.maxHeight, resizeOpts)
.png() .png()
.toFile(dstFile); .toFile(dstFile);
@ -69,7 +127,7 @@ module.exports = Self => {
}; };
await fs.mkdir(dstDir, {recursive: true}); await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false}) await sharp(imgSrc, sharpOptions)
.resize(size.width, size.height, resizeOpts) .resize(size.width, size.height, resizeOpts)
.png() .png()
.toFile(dstFile); .toFile(dstFile);

View File

@ -174,4 +174,9 @@ vn-table {
.vn-check { .vn-check {
margin: 0; 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) => { Self.lastActiveTickets = async(id, ticketId) => {
const ticket = await Self.app.models.Ticket.findById(ticketId); const ticket = await Self.app.models.Ticket.findById(ticketId);
const query = ` const query = `
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName, ad.city AS address SELECT
FROM vn.ticket t t.id,
JOIN vn.ticketState ts ON t.id = ts.ticketFk t.shipped,
JOIN vn.agencyMode a ON t.agencyModeFk = a.id a.name AS agencyName,
JOIN vn.warehouse w ON t.warehouseFk = w.id w.name AS warehouseName,
JOIN vn.address ad ON t.addressFk = ad.id ad.nickname AS nickname,
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0 ad.city AS city,
AND t.id <> ? AND t.warehouseFk = ? ad.postalCode AS postalCode,
ORDER BY t.shipped 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`; LIMIT 10`;
return Self.rawSql(query, [id, ticketId, ticket.warehouseFk]); 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}; return {'ic.id': value};
case 'salesPersonFk': case 'salesPersonFk':
return {'it.workerFk': value}; return {'it.workerFk': value};
case 'code':
return {'it.code': value};
case 'typeFk': case 'typeFk':
return {'i.typeFk': value}; return {'i.typeFk': value};
case 'active': case 'active':
@ -103,6 +105,7 @@ module.exports = Self => {
i.id AS itemFk, i.id AS itemFk,
i.size, i.size,
i.density, i.density,
it.code,
i.typeFk, i.typeFk,
i.family, i.family,
i.isActive, i.isActive,

View File

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

View File

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

View File

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

View File

@ -113,6 +113,12 @@
ng-model="$ctrl.item.stems" ng-model="$ctrl.item.stems"
rule> rule>
</vn-input-number> </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-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number

View File

@ -10,3 +10,4 @@ New intrastat: Nuevo intrastat
Identifier: Identificador Identifier: Identificador
Fragile: Frágil 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, ...) 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" model="model"
class="vn-w-xl vn-mb-xl"> class="vn-w-xl vn-mb-xl">
<vn-card> <vn-card>
<vn-table <vn-table
model="model" model="model"
show-fields="$ctrl.showFields" show-fields="$ctrl.showFields"
vn-smart-table="itemIndex"> vn-smart-table="itemIndex">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th> <vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th> <vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th> <vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="description">Description</vn-th> <vn-th field="name">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th> <vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th> <vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th> <vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="type" shrink>Type</vn-th> <vn-th field="typeFk" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th> <vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th> <vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th> <vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th> <vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th> <vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th> <vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th> <vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th> <vn-th></vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
<a ng-repeat="item in model.data" <a ng-repeat="item in model.data"
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})"> ui-sref="item.card.summary({id: item.id})">
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}" ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop vn-click-stop
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<span <span
vn-click-stop="itemDescriptor.show($event, item.id)" vn-click-stop="itemDescriptor.show($event, item.id)"
class="link"> class="link">
{{::item.id}} {{::item.id}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td> <vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td> <vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags> <vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one> <vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName"> <vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3> <h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one> </vn-one>
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="item" item="item"
tabindex="-1"> tabindex="-1">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>
<vn-td shrink>{{::item.stems}}</vn-td> <vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td> <vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td> <vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.type}}"> <vn-td shrink title="{{::item.typeName}}">
{{::item.type}} {{::item.typeName}}
</vn-td> </vn-td>
<vn-td shrink title="{{::item.category}}"> <vn-td shrink title="{{::item.category}}">
{{::item.category}} {{::item.category}}
</vn-td> </vn-td>
<vn-td shrink title="{{::item.intrastat}}"> <vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}} {{::item.intrastat}}
</vn-td> </vn-td>
<vn-td shrink>{{::item.origin}}</vn-td> <vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}"> <vn-td shrink title="{{::item.userName}}">
<span <span
class="link" class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)"> vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}} {{::item.userName}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink>{{::item.density}}</vn-td> <vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td> <vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-check <vn-check
disabled="true" disabled="true"
ng-model="::item.isActive"> ng-model="::item.isActive">
</vn-check> </vn-check>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-horizontal class="buttons"> <vn-horizontal class="buttons">
<vn-icon-button <vn-icon-button
vn-click-stop="clone.show(item.id)" vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone" vn-tooltip="Clone"
icon="icon-clone"> icon="icon-clone">
</vn-icon-button> </vn-icon-button>
<vn-icon-button <vn-icon-button
vn-click-stop="$ctrl.preview(item)" vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview" vn-tooltip="Preview"
icon="preview"> icon="preview">
</vn-icon-button> </vn-icon-button>
</vn-horizontal> </vn-horizontal>
</vn-td> </vn-td>
</a> </a>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>
<a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right> <a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
@ -128,3 +128,30 @@
item="$ctrl.itemSelected"> item="$ctrl.itemSelected">
</vn-item-summary> </vn-item-summary>
</vn-popup> </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) { onCloneAccept(itemFk) {
return this.$http.post(`Items/${itemFk}/clone`) return this.$http.post(`Items/${itemFk}/clone`)
.then(res => { .then(res => {

View File

@ -1,6 +1,6 @@
<div class="search-panel"> <div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()"> <form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal> <vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield <vn-textfield
vn-one vn-one
label="General search" label="General search"
@ -8,7 +8,7 @@
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal class="vn-px-lg">
<vn-textfield <vn-textfield
vn-one vn-one
label="Ticket id" label="Ticket id"
@ -25,7 +25,7 @@
<tpl-item>{{nickname}}</tpl-item> <tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal class="vn-px-lg">
<vn-textfield <vn-textfield
vn-one vn-one
label="Client id" label="Client id"
@ -38,19 +38,37 @@
url="Warehouses"> url="Warehouses">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker <vn-date-picker
vn-one vn-one
label="From" label="From"
ng-model="filter.from"> ng-model="filter.from"
on-change="$ctrl.from = value">
</vn-date-picker> </vn-date-picker>
<vn-date-picker <vn-date-picker
vn-one vn-one
label="To" label="To"
ng-model="filter.to"> ng-model="filter.to"
on-change="$ctrl.to = value">
</vn-date-picker> </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>
<vn-horizontal> </section>
<vn-horizontal class="vn-px-lg">
<vn-check vn-one <vn-check vn-one
triple-state="true" triple-state="true"
label="For me" label="For me"
@ -65,7 +83,7 @@
<tpl-item>{{name}}</tpl-item> <tpl-item>{{name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </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-submit label="Search"></vn-submit>
</vn-horizontal> </vn-horizontal>
</form> </form>

View File

@ -11,6 +11,35 @@ class Controller extends SearchPanel {
{code: 'denied', name: this.$t('Denied')} {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', { 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-portal slot="topbar">
<vn-searchbar <vn-searchbar
panel="vn-request-search-panel" panel="vn-request-search-panel"
suggested-filter="$ctrl.filterParams"
info="Search request by id or alias" info="Search request by id or alias"
suggested-filter="$ctrl.filterParams"
filter="$ctrl.filterParams" filter="$ctrl.filterParams"
fetch-params="$ctrl.fetchParams($params)"
model="model" model="model"
auto-state="false"> auto-state="false">
</vn-searchbar> </vn-searchbar>

View File

@ -15,7 +15,6 @@ export default class Controller extends Section {
nextWeek.setDate(nextWeek.getDate() + 7); nextWeek.setDate(nextWeek.getDate() + 7);
this.filterParams = { this.filterParams = {
mine: true,
from: today, from: today,
to: nextWeek, to: nextWeek,
state: 'pending' 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) { getState(isOk) {
if (isOk === null) if (isOk === null)
return 'Pending'; return 'Pending';

View File

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

View File

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

View File

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

View File

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

View File

@ -51,14 +51,29 @@ describe('Travel Component vnTravelCreate', () => {
expect(controller.travel.warehouseOutFk).toBeUndefined(); 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.`, () => { it(`should fill the fields when it's selected a date and agency.`, () => {
controller.travel = {agencyModeFk: 1}; controller.travel = {agencyModeFk: 1};
const tomorrow = new Date(); const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setDate(tomorrow.getDate() + 9);
const expectedResponse = { const expectedResponse = {
dayDuration: 2, id: 8,
warehouseInFk: 1, dayDuration: 9,
warehouseOutFk: 2 warehouseInFk: 5,
warehouseOutFk: 1
}; };
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`; const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;

View File

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

View File

@ -29,6 +29,15 @@
ng-model="$ctrl.worker.phone" ng-model="$ctrl.worker.phone"
rule> rule>
</vn-textfield> </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-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

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

View File

@ -30,6 +30,14 @@
<vn-label-value label="Department" <vn-label-value label="Department"
value="{{worker.department.department.name}}"> value="{{worker.department.department.name}}">
</vn-label-value> </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" <vn-label-value label="Phone"
value="{{worker.phone}}"> value="{{worker.phone}}">
</vn-label-value> </vn-label-value>
@ -51,3 +59,6 @@
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
</vn-card> </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; this.$.worker = null;
if (!value) return; if (!value) return;
let query = `Workers/${value.id}`; const query = `Workers/${value.id}`;
let filter = { const filter = {
include: [ include: [
{ {
relation: 'user', relation: 'user',
@ -31,13 +31,20 @@ class Controller extends Summary {
} }
}] }]
} }
}, { },
{
relation: 'client', relation: 'client',
scope: {fields: ['fi']} scope: {fields: ['fi']}
}, { },
{
relation: 'boss',
scope: {fields: ['id', 'nickname']}
},
{
relation: 'sip', relation: 'sip',
scope: {fields: ['extension']} scope: {fields: ['extension']}
}, { },
{
relation: 'department', relation: 'department',
scope: { scope: {
include: { include: {

32497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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