2863 - Added monitor module #618

Merged
joan merged 12 commits from 2863-monitor_module into dev 2021-05-12 12:09:35 +00:00
49 changed files with 1829 additions and 30 deletions

View File

@ -1,2 +1,4 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId)
VALUES ('SupplierAddress', '*', '*', 'ALLOW', 'ROLE', 'employee'); VALUES
('SupplierAddress', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('SalesMonitor', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -277,7 +277,7 @@ INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city
(106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'City of New York, New York, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1), (106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'City of New York, New York, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1),
(107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill, San Francisco, California', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1), (107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill, San Francisco, California', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(108, 'Charles Xavier', '22641921P', 'Professor X', 'Beast', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1), (108, 'Charles Xavier', '22641921P', 'Professor X', 'Beast', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1),
(109, 'Bruce Banner', '16104829E', 'Hulk', 'Black widow', 'Somewhere in New York', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1), (109, 'Bruce Banner', '16104829E', 'Hulk', 'Black widow', 'Somewhere in New York', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 9, 0, 1),
(110, 'Jessica Jones', '58282869H', 'Jessica Jones', 'Luke Cage', 'NYCC 2015 Poster', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1), (110, 'Jessica Jones', '58282869H', 'Jessica Jones', 'Luke Cage', 'NYCC 2015 Poster', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1),
(111, 'Missing', NULL, 'Missing man', 'Anton', 'The space, Universe far away', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1), (111, 'Missing', NULL, 'Missing man', 'Anton', 'The space, Universe far away', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1),
(112, 'Trash', NULL, 'Garbage man', 'Unknown name', 'New York city, Underground', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1); (112, 'Trash', NULL, 'Garbage man', 'Unknown name', 'New York city, Underground', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1);
@ -1559,6 +1559,66 @@ INSERT INTO `hedera`.`orderRowComponent`(`rowFk`, `componentFk`, `price`)
(30, 37, 2), (30, 37, 2),
(30, 39, 0.01); (30, 39, 0.01);
INSERT INTO `hedera`.`visit`(`id`, `firstAgentFk`)
VALUES
(1, NULL),
(2, NULL),
(3, NULL),
(4, NULL),
(5, NULL),
(6, NULL),
(7, NULL),
(8, NULL),
(9, NULL);
INSERT INTO `hedera`.`visitAgent`(`id`, `visitFk`)
VALUES
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9);
INSERT INTO `hedera`.`visitAccess`(`id`, `agentFk`, `stamp`)
VALUES
(1, 1, CURDATE()),
(2, 2, CURDATE()),
(3, 3, CURDATE()),
(4, 4, CURDATE()),
(5, 5, CURDATE()),
(6, 6, CURDATE()),
(7, 7, CURDATE()),
(8, 8, CURDATE()),
(9, 9, CURDATE());
INSERT INTO `hedera`.`visitUser`(`id`, `accessFk`, `userFk`, `stamp`)
VALUES
(1, 1, 101, CURDATE()),
(2, 2, 101, CURDATE()),
(3, 3, 101, CURDATE()),
(4, 4, 102, CURDATE()),
(5, 5, 102, CURDATE()),
(6, 6, 102, CURDATE()),
(7, 7, 103, CURDATE()),
(8, 8, 103, CURDATE()),
(9, 9, 103, CURDATE());
INSERT INTO `hedera`.`userSession`(`created`, `lastUpdate`, `ssid`, `data`, `userVisitFk`)
VALUES
(CURDATE(), CURDATE(), '121', 'data', 1),
(CURDATE(), CURDATE(), '122', 'data', 2),
(CURDATE(), CURDATE(), '123', 'data', 3),
(CURDATE(), CURDATE(), '124', 'data', 4),
(CURDATE(), CURDATE(), '125', 'data', 5),
(CURDATE(), CURDATE(), '126', 'data', 6),
(CURDATE(), CURDATE(), '127', 'data', 7),
(CURDATE(), CURDATE(), '128', 'data', 8),
(CURDATE(), CURDATE(), '129', 'data', 9);
INSERT INTO `vn`.`clientContact`(`id`, `clientFk`, `name`, `phone`) INSERT INTO `vn`.`clientContact`(`id`, `clientFk`, `name`, `phone`)
VALUES VALUES
(1, 101, 'contact 1', 666777888), (1, 101, 'contact 1', 666777888),
@ -1688,11 +1748,12 @@ INSERT INTO `vn`.`receipt`(`id`, `invoiceFk`, `amountPaid`, `amountUnpaid`, `pay
INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`) INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`)
VALUES VALUES
(1, 1, 9), (1, 1, 9),
(2, 1, 18), (2, 2, 18),
(3, 2, 101), (3, 2, 19),
(4, 2, 102), (4, 3, 101),
(5, 3, 103), (5, 3, 102),
(6, 3, 104); (6, 4, 103),
(7, 4, 104);
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `attenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`) INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `attenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
VALUES VALUES

View File

@ -123,4 +123,12 @@
} }
} }
} }
&.small {
height: 24px;
font-size: .75rem;
& > button {
padding: 0 6px;
}
}
} }

View File

@ -23,9 +23,15 @@ export default class Table {
this.model.refresh(); this.model.refresh();
} }
$onChanges() { isScrollable() {
if (this.model && !this.model.data) return this.table.classList.contains('scrollable');
this.applyOrder(); }
$postLink() {
if (this.isScrollable()) {
const childCells = this.table.querySelector('vn-tbody');
console.log(childCells);
}
} }
setActiveArrow() { setActiveArrow() {

View File

@ -20,7 +20,6 @@ vn-table {
font-weight: normal; font-weight: normal;
} }
& > * > vn-th[field] { & > * > vn-th[field] {
position: relative;
overflow: visible; overflow: visible;
cursor: pointer cursor: pointer
} }
@ -83,6 +82,11 @@ vn-table {
} }
&[shrink-date] { &[shrink-date] {
width: 100px; width: 100px;
max-width: 100px;
}
&[shrink-datetime] {
width: 150px;
max-width: 150px;
} }
&[expand] { &[expand] {
max-width: 400px; max-width: 400px;
@ -183,3 +187,35 @@ vn-table {
text-align: center; text-align: center;
} }
} }
vn-table.scrollable,
vn-table.scrollable > .vn-table,
.vn-table.scrollable {
border-collapse: separate;
overflow: auto;
vn-thead, thead {
border-bottom: 0px solid transparent
}
vn-thead th,
vn-thead vn-th,
thead vn-th,
thead th {
border-bottom: 2px solid $color-spacer;
background-color: #FFF;
position: sticky;
top: 0
}
}
vn-table.scrollable.sm,
.vn-table.scrollable.sm {
max-height: 300px
}
vn-table.scrollable.lg,
.vn-table.scrollable.lg {
max-height: 700px
}

View File

@ -14,8 +14,11 @@ export default class Th {
$onInit() { $onInit() {
if (!this.field) return; if (!this.field) return;
if (this.defaultOrder) if (this.defaultOrder) {
this.order = this.defaultOrder; this.order = this.defaultOrder;
this.table.applyOrder(this.field, this.order);
this.updateArrow();
}
this.updateArrow(); this.updateArrow();
} }

View File

@ -16,10 +16,11 @@ export default function moduleImport(moduleName) {
case 'travel' : return import('travel/front'); case 'travel' : return import('travel/front');
case 'worker' : return import('worker/front'); case 'worker' : return import('worker/front');
case 'invoiceOut' : return import('invoiceOut/front'); case 'invoiceOut' : return import('invoiceOut/front');
case 'invoiceIn' : return import('invoiceIn/front'); case 'invoiceIn' : return import('invoiceIn/front');
case 'route' : return import('route/front'); case 'route' : return import('route/front');
case 'entry' : return import('entry/front'); case 'entry' : return import('entry/front');
case 'account' : return import('account/front'); case 'account' : return import('account/front');
case 'supplier' : return import('supplier/front'); case 'supplier' : return import('supplier/front');
case 'monitor' : return import('monitor/front');
} }
} }

View File

@ -47,6 +47,7 @@ Invoices in: Fact. recibidas
Entries: Entradas Entries: Entradas
Users: Usuarios Users: Usuarios
Suppliers: Proveedores Suppliers: Proveedores
Monitors: Monitores
# Common # Common

View File

@ -2,7 +2,8 @@
vn-id="model" vn-id="model"
url="Buys/latestBuysFilter" url="Buys/latestBuysFilter"
limit="20" limit="20"
data="$ctrl.buys"> data="$ctrl.buys"
auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar

View File

@ -66,6 +66,13 @@ describe('Entry', () => {
describe('onEditAccept()', () => { describe('onEditAccept()', () => {
it(`should perform a query to update columns`, () => { it(`should perform a query to update columns`, () => {
$httpBackend.whenGET('UserConfigViews/getConfig?tableCode=latestBuys').respond([]);
$httpBackend.whenGET('Buys/latestBuysFilter?filter=%7B%22limit%22:20%7D').respond([
{entryFk: 1},
{entryFk: 2},
{entryFk: 3},
{entryFk: 4}
]);
controller.editedColumn = {field: 'my field', newValue: 'the new value'}; controller.editedColumn = {field: 'my field', newValue: 'the new value'};
let query = 'Buys/editLatestBuys'; let query = 'Buys/editLatestBuys';

View File

@ -7,7 +7,7 @@
"menus": { "menus": {
"main": [ "main": [
{"state": "entry.index", "icon": "icon-entry"}, {"state": "entry.index", "icon": "icon-entry"},
{"state": "entry.latestBuys", "icon": "icon-latestBuys"} {"state": "entry.latestBuys", "icon": "contact_support"}
], ],
"card": [ "card": [
{"state": "entry.card.basicData", "icon": "settings"}, {"state": "entry.card.basicData", "icon": "settings"},

View File

@ -0,0 +1,60 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('clientsFilter', {
description: 'Find all instances of the model matched by filter from the data source.',
accepts: [
{
arg: 'ctx',
type: 'object',
http: {source: 'context'}
},
{
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/clientsFilter',
verb: 'GET'
}
});
Self.clientsFilter = async(ctx, filter) => {
const userId = ctx.req.accessToken.userId;
const conn = Self.dataSource.connector;
const stmt = new ParameterizedSQL(`
SELECT
u.name AS salesPerson,
IFNULL(sc.workerSubstitute, c.salesPersonFk) AS salesPersonFk,
c.id AS clientFk,
c.name AS clientName,
s.lastUpdate AS dated,
wtc.workerFk
FROM hedera.userSession s
JOIN hedera.visitUser v ON v.id = s.userVisitFk
JOIN client c ON c.id = v.userFk
LEFT JOIN account.user u ON c.salesPersonFk = u.id
LEFT JOIN worker w ON c.salesPersonFk = w.id
LEFT JOIN sharingCart sc ON sc.workerFk = c.salesPersonFk
AND CURDATE() BETWEEN sc.started AND sc.ended
LEFT JOIN workerTeamCollegues wtc
ON wtc.collegueFk = IFNULL(sc.workerSubstitute, c.salesPersonFk)`);
if (!filter.where) filter.where = {};
const where = filter.where;
where['wtc.workerFk'] = userId;
stmt.merge(conn.makeSuffix(filter));
return conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,68 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('ordersFilter', {
description: 'Find all instances of the model matched by filter from the data source.',
accepts: [
{
arg: 'ctx',
type: 'object',
http: {source: 'context'}
},
{
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/ordersFilter',
verb: 'GET'
}
});
Self.ordersFilter = async(ctx, filter) => {
const userId = ctx.req.accessToken.userId;
const conn = Self.dataSource.connector;
const stmt = new ParameterizedSQL(`
SELECT
c.id AS clientFk,
c.name AS clientName,
a.nickname,
o.id,
o.date_make,
o.date_send,
o.customer_id,
COUNT(item_id) AS totalRows,
ROUND(SUM(amount * price)) * 1 AS import,
u.id AS salesPersonFk,
u.name AS salesPerson,
am.name AS agencyName
FROM hedera.order o
JOIN hedera.order_row orw ON o.id = orw.order_id
JOIN client c ON c.id = o.customer_id
JOIN address a ON a.id = o.address_id
JOIN agencyMode am ON am.id = o.agency_id
JOIN user u ON u.id = c.salesPersonFk
JOIN workerTeamCollegues wtc ON c.salesPersonFk = wtc.collegueFk`);
if (!filter.where) filter.where = {};
const where = filter.where;
where['o.confirmed'] = false;
where['o.date_send'] = {gt: '2001-01-01'};
where['wtc.workerFk'] = userId;
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makeGroupBy('o.id'));
stmt.merge(conn.makeLimit(filter));
return conn.executeStmt(stmt);
};
};

View File

@ -0,0 +1,319 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('salesFilter', {
description: 'Find all instances of the model matched by filter from the data source.',
accepts: [
{
arg: 'ctx',
type: 'object',
http: {source: 'context'}
}, {
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}, {
arg: 'search',
type: 'string',
description: `If it's and number searchs by id, otherwise it searchs by nickname`
}, {
arg: 'from',
type: 'date',
description: `The from date filter`
}, {
arg: 'to',
type: 'date',
description: `The to date filter`
}, {
arg: 'nickname',
type: 'string',
description: `The nickname filter`
}, {
arg: 'id',
type: 'number',
description: `The ticket id filter`
}, {
arg: 'clientFk',
type: 'number',
description: `The client id filter`
}, {
arg: 'agencyModeFk',
type: 'number',
description: `The agency mode id filter`
}, {
arg: 'warehouseFk',
type: 'number',
description: `The warehouse id filter`
}, {
arg: 'salesPersonFk',
type: 'number',
description: `The salesperson id filter`
}, {
arg: 'provinceFk',
type: 'number',
description: `The province id filter`
}, {
arg: 'stateFk',
type: 'number',
description: `The state id filter`
}, {
arg: 'myTeam',
type: 'boolean',
description: `Whether to show only tickets for the current logged user team (For now it shows only the current user tickets)`
}, {
arg: 'problems',
type: 'boolean',
description: `Whether to show only tickets with problems`
}, {
arg: 'pending',
type: 'boolean',
description: `Whether to show only tickets with state 'Pending'`
}, {
arg: 'mine',
type: 'boolean',
description: `Whether to show only tickets for the current logged user`
}, {
arg: 'orderFk',
type: 'number',
description: `The order id filter`
}, {
arg: 'refFk',
type: 'string',
description: `The invoice reference filter`
}, {
arg: 'alertLevel',
type: 'number',
description: `The alert level of the tickets`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/salesFilter',
verb: 'GET'
}
});
Self.salesFilter = async(ctx, filter) => {
const userId = ctx.req.accessToken.userId;
const conn = Self.dataSource.connector;
const models = Self.app.models;
const args = ctx.args;
// Apply filter by team
const teamMembersId = [];
if (args.myTeam != null) {
const worker = await models.Worker.findById(userId, {
include: {
relation: 'collegues'
}
});
const collegues = worker.collegues() || [];
collegues.forEach(collegue => {
teamMembersId.push(collegue.collegueFk);
});
if (teamMembersId.length == 0)
teamMembersId.push(userId);
}
if (ctx.args && args.to) {
const dateTo = args.to;
dateTo.setHours(23, 59, 0, 0);
}
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': {inq: value}}
: {'t.nickname': {like: `%${value}%`}};
case 'from':
return {'t.shipped': {gte: value}};
case 'to':
return {'t.shipped': {lte: value}};
case 'nickname':
return {'t.nickname': {like: `%${value}%`}};
case 'refFk':
return {'t.refFk': value};
case 'salesPersonFk':
return {'c.salesPersonFk': value};
case 'provinceFk':
return {'a.provinceFk': value};
case 'stateFk':
return {'ts.stateFk': value};
case 'mine':
case 'myTeam':
if (value)
return {'c.salesPersonFk': {inq: teamMembersId}};
else
return {'c.salesPersonFk': {nin: teamMembersId}};
case 'alertLevel':
return {'ts.alertLevel': value};
case 'pending':
if (value) {
return {and: [
{'st.alertLevel': 0},
{'st.code': {nin: [
'OK',
'BOARDING',
'PRINTED',
'PRINTED_AUTO',
'PICKER_DESIGNED'
]}}
]};
} else {
return {and: [
{'st.alertLevel': {gt: 0}}
]};
}
case 'id':
case 'clientFk':
case 'agencyModeFk':
case 'warehouseFk':
param = `t.${param}`;
return {[param]: value};
}
});
filter = mergeFilters(filter, {where});
let stmts = [];
let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.filter');
stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
ENGINE = MEMORY
SELECT
t.id,
t.shipped,
CAST(DATE(t.shipped) AS CHAR) AS shippedDate,
HOUR(t.shipped) AS shippedHour,
t.nickname,
t.refFk,
t.routeFk,
t.warehouseFk,
t.clientFk,
t.totalWithoutVat,
t.totalWithVat,
io.id AS invoiceOutId,
a.provinceFk,
p.name AS province,
w.name AS warehouse,
am.name AS agencyMode,
am.id AS agencyModeFk,
st.name AS state,
wk.lastName AS salesPerson,
ts.stateFk AS stateFk,
ts.alertLevel AS alertLevel,
ts.code AS alertLevelCode,
u.name AS userName,
c.salesPersonFk,
z.hour AS zoneLanding,
HOUR(z.hour) AS zoneHour,
MINUTE(z.hour) AS zoneMinute,
z.name AS zoneName,
z.id AS zoneFk,
CAST(z.hour AS CHAR) AS hour
FROM ticket t
LEFT JOIN invoiceOut io ON t.refFk = io.ref
LEFT JOIN zone z ON z.id = t.zoneFk
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN province p ON p.id = a.provinceFk
LEFT JOIN warehouse w ON w.id = t.warehouseFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.userFk`);
if (args.orderFk) {
stmt.merge({
sql: `JOIN orderTicket ot ON ot.ticketFk = t.id AND ot.orderFk = ?`,
params: [args.orderFk]
});
}
stmt.merge(conn.makeWhere(filter.where));
stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems');
stmts.push(`
CREATE TEMPORARY TABLE tmp.ticketGetProblems
(INDEX (ticketFk))
ENGINE = MEMORY
SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped
FROM tmp.filter f
LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel
WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)
AND f.shipped >= CURDATE()`);
stmts.push('CALL ticketGetProblems(FALSE)');
stmt = new ParameterizedSQL(`
SELECT
f.*,
tp.*
FROM tmp.filter f
LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id`);
if (args.problems != undefined && (!args.from && !args.to))
throw new UserError('Choose a date range or days forward');
let condition;
let hasProblem;
let range;
let hasWhere;
switch (args.problems) {
case true:
condition = `or`;
hasProblem = true;
range = 0;
hasWhere = true;
break;
case false:
condition = `and`;
hasProblem = null;
range = null;
hasWhere = true;
break;
}
let problems = {[condition]: [
{'tp.isFreezed': hasProblem},
{'tp.risk': hasProblem},
{'tp.hasTicketRequest': hasProblem},
{'tp.isAvailable': range}
]};
if (hasWhere)
stmt.merge(conn.makeWhere(problems));
stmt.merge(conn.makeOrderBy(filter.order));
stmt.merge(conn.makeLimit(filter));
let ticketsIndex = stmts.push(stmt) - 1;
stmts.push(
`DROP TEMPORARY TABLE
tmp.filter,
tmp.ticket,
tmp.ticketGetProblems`);
let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql);
return result[ticketsIndex];
};
};

View File

@ -0,0 +1,11 @@
const app = require('vn-loopback/server/server');
describe('SalesMonitor clientsFilter()', () => {
it('should return the clients web activity', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {}};
const filter = {order: 'dated DESC'};
const result = await app.models.SalesMonitor.clientsFilter(ctx, filter);
expect(result.length).toEqual(9);
});
});

View File

@ -0,0 +1,11 @@
const app = require('vn-loopback/server/server');
describe('SalesMonitor ordersFilter()', () => {
it('should return the orders activity', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {}};
const filter = {order: 'dated DESC'};
const result = await app.models.SalesMonitor.ordersFilter(ctx, filter);
expect(result.length).toEqual(12);
});
});

View File

@ -0,0 +1,106 @@
const app = require('vn-loopback/server/server');
describe('SalesMonitor salesFilter()', () => {
it('should return the tickets matching the filter', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {}};
const filter = {order: 'id DESC'};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(24);
});
it('should return the tickets matching the problems on true', async() => {
const yesterday = new Date();
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(23, 59, 59, 59);
const ctx = {req: {accessToken: {userId: 9}}, args: {
problems: true,
from: yesterday,
to: today
}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(3);
});
it('should return the tickets matching the problems on false', async() => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(23, 59, 59, 59);
const ctx = {req: {accessToken: {userId: 9}}, args: {
problems: false,
from: yesterday,
to: today
}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(7);
});
it('should return the tickets matching the problems on null', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {problems: null}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(24);
});
it('should return the tickets matching the orderId 11', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {orderFk: 11}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
const firstRow = result[0];
expect(result.length).toEqual(1);
expect(firstRow.id).toEqual(11);
});
it('should return the tickets with grouped state "Pending" and not "Ok" nor "BOARDING"', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: true}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
expect(length).toEqual(7);
expect(anyResult.state).toMatch(/(Libre|Arreglar)/);
});
it('should return the tickets that are not pending', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: false}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
const firstRow = result[0];
const secondRow = result[1];
const thirdRow = result[2];
expect(result.length).toEqual(12);
expect(firstRow.state).toEqual('Entregado');
expect(secondRow.state).toEqual('Entregado');
expect(thirdRow.state).toEqual('Entregado');
});
it('should return the tickets from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: true}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(20);
});
it('should return the tickets that are not from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: false}};
const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(4);
});
});

View File

@ -0,0 +1,5 @@
{
"SalesMonitor": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,5 @@
module.exports = Self => {
require('../methods/sales-monitor/salesFilter')(Self);
require('../methods/sales-monitor/clientsFilter')(Self);
require('../methods/sales-monitor/ordersFilter')(Self);
};

View File

@ -0,0 +1,11 @@
{
"name": "SalesMonitor",
"base": "VnModel",
"acls": [{
"property": "status",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1,8 @@
export * from './module';
import './main';
import './index/';
import './index/tickets';
import './index/clients';
import './index/orders';
import './index/search-panel';

View File

@ -0,0 +1,101 @@
<vn-crud-model auto-load="true"
vn-id="model"
url="SalesMonitors/clientsFilter"
limit="6"
order="dated DESC">
</vn-crud-model>
<vn-horizontal class="header">
<vn-one translate>
Clients on website
</vn-one>
<vn-none>
<vn-icon
icon="refresh"
vn-tooltip="Refresh"
ng-click="model.refresh()">
</vn-icon>
</vn-none>
</vn-horizontal>
<vn-card>
<vn-table model="model" class="scrollable sm">
<vn-thead>
<vn-tr>
<vn-th field="dated">Hour</vn-th>
<vn-th field="salesPersonFk" class="expendable">Salesperson</vn-th>
<vn-th field="clientFk">Client</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="visit in model.data">
<vn-td shrink-date>
<span class="chip">
{{::visit.dated | date: 'HH:mm'}}
</span>
</vn-td>
<vn-td class="shrink expendable">
<span
title="{{::visit.salesPerson}}"
vn-click-stop="workerDescriptor.show($event, visit.salesPersonFk)"
class="link">
{{::visit.salesPerson | dashIfEmpty}}
</span>
</vn-td>
<vn-td>
<span
title="{{::visit.clientName}}"
vn-click-stop="clientDescriptor.show($event, visit.clientFk)"
class="link">
{{::visit.clientName}}
</span>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div
ng-if="!model.data.length"
class="empty-rows vn-pa-sm"
translate>
No results
</div>
<vn-pagination
model="model"
class="vn-pt-xs"
scroll-selector="vn-monitor-sales-clients vn-table"
scroll-offset="100">
</vn-pagination>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-contextmenu vn-id="contextmenu" targets="['vn-monitor-sales-clients vn-table']" 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>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -0,0 +1,30 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
case 'dated':
return {'s.lastUpdate': {
between: this.dateRange(value)}
};
case 'clientFk':
case 'salesPersonFk':
return {[`c.${param}`]: value};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
}
ngModule.vnComponent('vnMonitorSalesClients', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,13 @@
<vn-horizontal>
<vn-three class="vn-mr-sm">
<vn-monitor-sales-tickets></vn-monitor-sales-tickets>
</vn-three>
<vn-one>
<vn-vertical class="vn-mb-sm">
<vn-monitor-sales-clients></vn-monitor-sales-clients>
</vn-vertical>
<vn-vertical>
<vn-monitor-sales-orders></vn-monitor-sales-orders>
</vn-vertical>
</vn-one>
</vn-horizontal>

View File

@ -0,0 +1,8 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
ngModule.vnComponent('vnMonitorIndex', {
template: require('./index.html'),
controller: Section
});

View File

@ -0,0 +1,4 @@
Tickets monitor: Monitor de tickets
Clients on website: Clientes activos en la web
Recent order actions: Acciones recientes en pedidos
Search tickets: Buscar tickets

View File

@ -0,0 +1,85 @@
<vn-crud-model auto-load="true"
vn-id="model"
url="SalesMonitors/ordersFilter"
limit="6"
order="dated DESC">
</vn-crud-model>
<vn-horizontal class="header">
<vn-one translate>
Recent order actions
</vn-one>
<vn-none>
<vn-icon
icon="refresh"
vn-tooltip="Refresh"
ng-click="model.refresh()">
</vn-icon>
</vn-none>
</vn-horizontal>
<vn-card>
<vn-table model="model" class="scrollable sm">
<vn-thead>
<vn-tr>
<vn-th field="date_send" shrink-datetime default-order="DESC">Date</vn-th>
<vn-th field="clientFk">Client</vn-th>
<vn-th>Import</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody ng-repeat="order in model.data">
<vn-tr>
<vn-td>
<span class="chip success">
{{::order.date_send | date: 'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td>
<span
title="{{::order.clientName}}"
vn-click-stop="clientDescriptor.show($event, order.clientFk)"
class="link">
{{::order.clientName}}
</span>
</vn-td>
<vn-td number>{{::order.import | currency: 'EUR':2}}</vn-td>
</vn-tr>
<vn-tr class="dark-row">
<vn-td shrink-datetime>
<span>
{{::order.date_make | date: 'dd/MM/yyyy HH:mm'}}
</span>
</vn-td>
<vn-td>
<span title="{{::order.agencyName}}">
{{::order.agencyName | dashIfEmpty}}
</span>
</vn-td>
<vn-td>
<span
title="{{::order.salesPerson}}"
vn-click-stop="workerDescriptor.show($event, order.salesPersonFk)"
class="link">
{{::order.salesPerson | dashIfEmpty}}
</span>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div
ng-if="!model.data.length"
class="empty-rows vn-pa-sm"
translate>
No results
</div>
<vn-pagination
model="model"
class="vn-pt-xs"
scroll-selector="vn-monitor-sales-orders vn-table"
scroll-offset="100">
</vn-pagination>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>

View File

@ -0,0 +1,8 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
import './style.scss';
ngModule.vnComponent('vnMonitorSalesOrders', {
template: require('./index.html'),
controller: Section
});

View File

@ -0,0 +1,16 @@
@import "variables";
vn-monitor-sales-orders {
.dark-row {
background-color: lighten($color-marginal, 15%);
color: gray;
vn-td {
border-bottom: 2px solid $color-marginal
}
}
vn-tbody vn-tr:nth-child(3) {
height: inherit
}
}

View File

@ -0,0 +1,138 @@
<div class="search-panel">
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search ticket by id or alias"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Client id"
ng-model="filter.clientFk">
</vn-textfield>
<vn-textfield
vn-one
label="Order id"
ng-model="filter.orderFk">
</vn-textfield>
</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"
on-change="$ctrl.from = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="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>
</section>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Nickname"
ng-model="filter.nickname">
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="filter.salesPersonFk"
url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'employee'}"
label="Sales person">
<tpl-item>{{firstName}} {{name}}</tpl-item>
</vn-autocomplete>
<vn-textfield
vn-one
label="Invoice"
ng-model="filter.refFk">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes/isActive">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="State"
ng-model="filter.stateFk"
url="States">
</vn-autocomplete>
<vn-autocomplete vn-one
data="$ctrl.groupedStates"
label="Grouped States"
value-field="alertLevel"
show-field="name"
ng-model="filter.alertLevel">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="filter.warehouseFk"
url="Warehouses">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Province"
ng-model="filter.provinceFk"
url="Provinces">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-check
vn-one
label="My team"
ng-model="filter.myTeam"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="With problems"
ng-model="filter.problems"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="Pending"
ng-model="filter.pending"
triple-state="true">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,59 @@
import ngModule from '../../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
this.getGroupedStates();
}
getGroupedStates() {
let groupedStates = [];
this.$http.get('AlertLevels').then(res => {
for (let state of res.data) {
groupedStates.push({
alertLevel: state.alertLevel,
code: state.code,
name: this.$t(state.code)
});
}
this.groupedStates = groupedStates;
});
}
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('vnMonitorSalesSearchPanel', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,71 @@
import './index';
describe('Monitor Component vnMonitorSalesSearchPanel', () => {
let $httpBackend;
let controller;
beforeEach(ngModule('monitor'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnMonitorSalesSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('getGroupedStates()', () => {
it('should set an array of groupedStates with the adition of a name translation', () => {
jest.spyOn(controller, '$t').mockReturnValue('miCodigo');
const data = [
{
alertLevel: 9999,
code: 'myCode'
}
];
$httpBackend.whenGET('AlertLevels').respond(data);
controller.getGroupedStates();
$httpBackend.flush();
expect(controller.groupedStates).toEqual([{
alertLevel: 9999,
code: 'myCode',
name: 'miCodigo'
}]);
});
});
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

@ -0,0 +1,20 @@
@import "variables";
@import "effects";
vn-monitor-index {
.header {
padding: 12px 0;
color: gray;
font-size: 1.2rem;
& > vn-none > vn-icon {
@extend %clickable-light;
color: $color-button;
font-size: 1.4rem;
}
}
.empty-rows {
color: $color-font-secondary;
text-align: center;
}
}

View File

@ -0,0 +1,206 @@
<vn-crud-model auto-load="true"
vn-id="model"
params="::$ctrl.filterParams"
url="SalesMonitors/salesFilter"
limit="20"
order="shippedDate DESC, shippedHour ASC, zoneLanding ASC, id">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-monitor-sales-search-panel"
placeholder="Search tickets"
info="Search ticket by id or alias"
model="model"
fetch-params="$ctrl.fetchParams($params)"
suggested-filter="$ctrl.filterParams"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-horizontal class="header">
<vn-one translate>
Tickets monitor
</vn-one>
<vn-none>
<vn-icon
icon="refresh"
vn-tooltip="Refresh"
ng-click="model.refresh()">
</vn-icon>
</vn-none>
</vn-horizontal>
<vn-card>
<vn-table model="model" class="scrollable lg">
<vn-thead>
<vn-tr>
<vn-th class="icon-field"></vn-th>
<vn-th field="nickname" expand>Client</vn-th>
<vn-th field="salesPersonFk" class="expendable" shrink>Salesperson</vn-th>
<vn-th field="shipped" shrink-date>Date</vn-th>
<vn-th>Hour</vn-th>
<vn-th field="hour" shrink>Closure</vn-th>
<vn-th field="provinceFk" class="expendable">Province</vn-th>
<vn-th field="stateFk" shrink>State</vn-th>
<vn-th field="zoneFk">Zone</vn-th>
<vn-th shrink>Total</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="ticket in model.data"
class="clickable vn-tr search-result"
ui-sref="ticket.card.summary({id: {{::ticket.id}}})">
<vn-td class="icon-field">
<vn-icon
ng-show="::ticket.isTaxDataChecked === 0"
translate-attr="{title: 'No verified data'}"
class="bright"
icon="icon-no036">
</vn-icon>
<vn-icon
ng-show="::ticket.hasTicketRequest"
translate-attr="{title: 'Purchase request'}"
class="bright"
icon="icon-100">
</vn-icon>
<vn-icon
ng-show="::ticket.isAvailable === 0"
translate-attr="{title: 'Not available'}"
class="bright"
vn-tooltip="Not available"
icon="icon-unavailable">
</vn-icon>
<vn-icon
ng-show="::ticket.isFreezed"
translate-attr="{title: 'Client frozen'}"
class="bright"
icon="icon-frozen">
</vn-icon>
<vn-icon
ng-show="::ticket.risk"
title="{{::$ctrl.$t('Risk')}}: {{ticket.risk}}"
class="bright"
icon="icon-risk">
</vn-icon>
</vn-td>
<vn-td expand>
<span
title="{{::ticket.nickname}}"
vn-click-stop="clientDescriptor.show($event, ticket.clientFk)"
class="link">
{{::ticket.nickname}}
</span>
</vn-td>
<vn-td class="expendable" shrink>
<span
title="{{::ticket.userName}}"
vn-click-stop="workerDescriptor.show($event, ticket.salesPersonFk)"
class="link">
{{::ticket.userName | dashIfEmpty}}
</span>
</vn-td>
<vn-td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.shipped)}}">
{{::ticket.shipped | date: 'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td shrink>{{::ticket.shipped | date: 'HH:mm'}}</vn-td>
<vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td>
<vn-td class="expendable">{{::ticket.province}}</vn-td>
<vn-td class="expendable" shrink>
<span
ng-show="::ticket.refFk"
title="{{::ticket.refFk}}"
vn-click-stop="invoiceOutDescriptor.show($event, ticket.invoiceOutId)"
class="link">
{{::ticket.refFk}}
</span>
<span
ng-show="::!ticket.refFk"
class="chip {{$ctrl.stateColor(ticket)}}">
{{ticket.state}}
</span>
</vn-td>
<vn-td>
<span
title="{{::ticket.zoneName}}"
vn-click-stop="zoneDescriptor.show($event, ticket.zoneFk)"
class="link">
{{::ticket.zoneName | dashIfEmpty}}
</span>
</vn-td>
<vn-td shrink>
<span class="chip {{$ctrl.totalPriceColor(ticket)}}">
{{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
</span>
</vn-td>
<vn-td actions>
<vn-icon-button
vn-anchor="::{
state: 'ticket.card.sale',
params: {id: ticket.id},
target: '_blank'
}"
vn-tooltip="Go to lines"
icon="icon-lines">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(ticket)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-td>
</a>
</vn-tbody>
</vn-table>
<vn-pagination
model="model"
class="vn-pt-xs"
scroll-selector="vn-monitor-sales-tickets vn-table"
scroll-offset="100">
</vn-pagination>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-zone-descriptor-popover
vn-id="zoneDescriptor">
</vn-zone-descriptor-popover>
<vn-popup vn-id="summary">
<vn-ticket-summary
ticket="$ctrl.selectedTicket"
model="model">
</vn-ticket-summary>
</vn-popup>
<vn-contextmenu vn-id="contextmenu" targets="['vn-monitor-sales-tickets vn-table']" 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>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -0,0 +1,105 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filterParams = this.fetchParams({
scopeDays: 1
});
}
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;
}
compareDate(date) {
let today = new Date();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0)
return 'warning';
if (comparation < 0)
return 'success';
}
stateColor(ticket) {
if (ticket.alertLevelCode === 'OK')
return 'success';
else if (ticket.alertLevelCode === 'FREE')
return 'notice';
else if (ticket.alertLevel === 1)
return 'warning';
else if (ticket.alertLevel === 0)
return 'alert';
}
totalPriceColor(ticket) {
const total = parseInt(ticket.totalWithVat);
if (total > 0 && total < 50)
return 'warning';
}
exprBuilder(param, value) {
switch (param) {
case 'stateFk':
return {'ts.stateFk': value};
case 'salesPersonFk':
return {'c.salesPersonFk': value};
case 'provinceFk':
return {'a.provinceFk': value};
case 'hour':
return {'z.hour': value};
case 'shipped':
return {'t.shipped': {
between: this.dateRange(value)}
};
case 'id':
case 'refFk':
case 'zoneFk':
case 'nickname':
case 'agencyModeFk':
case 'warehouseFk':
return {[`t.${param}`]: value};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
preview(ticket) {
this.selectedTicket = ticket;
this.$.summary.show();
}
}
ngModule.vnComponent('vnMonitorSalesTickets', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,133 @@
import './index.js';
describe('Component vnMonitorSalesTickets', () => {
let controller;
let $window;
let tickets = [{
id: 1,
clientFk: 1,
checked: false,
totalWithVat: 10.5
}, {
id: 2,
clientFk: 1,
checked: true,
totalWithVat: 20.5
}, {
id: 3,
clientFk: 1,
checked: true,
totalWithVat: 30
}];
beforeEach(ngModule('monitor'));
beforeEach(inject(($componentController, _$window_) => {
$window = _$window_;
const $element = angular.element('<vn-monitor-sales-tickets></vn-monitor-sales-tickets>');
controller = $componentController('vnMonitorSalesTickets', {$element});
}));
describe('fetchParams()', () => {
it('should return a range of dates with passed scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
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);
const expectedParams = {
from,
scopeDays: params.scopeDays,
to
};
expect(params).toEqual(expectedParams);
});
it('should return default value for scope days', () => {
let params = controller.fetchParams({
scopeDays: 1
});
expect(params.scopeDays).toEqual(1);
});
it('should return the given scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
expect(params.scopeDays).toEqual(2);
});
});
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let today = new Date();
let result = controller.compareDate(today);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = new Date();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = new Date();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('stateColor()', () => {
it('should return "success" when the alertLevelCode property is "OK"', () => {
const result = controller.stateColor({alertLevelCode: 'OK'});
expect(result).toEqual('success');
});
it('should return "notice" when the alertLevelCode property is "FREE"', () => {
const result = controller.stateColor({alertLevelCode: 'FREE'});
expect(result).toEqual('notice');
});
it('should return "warning" when the alertLevel property is "1', () => {
const result = controller.stateColor({alertLevel: 1});
expect(result).toEqual('warning');
});
it('should return "alert" when the alertLevel property is "0"', () => {
const result = controller.stateColor({alertLevel: 0});
expect(result).toEqual('alert');
});
});
describe('preview()', () => {
it('should show the dialog summary', () => {
controller.$.summary = {show: () => {}};
jest.spyOn(controller.$.summary, 'show');
let event = new MouseEvent('click', {
view: $window,
bubbles: true,
cancelable: true
});
controller.preview(event, tickets[0]);
expect(controller.$.summary.show).toHaveBeenCalledWith();
});
});
});

View File

@ -0,0 +1,23 @@
vn-monitor-sales-tickets {
@media screen and (max-width: 1440px) {
.expendable {
display: none;
}
}
vn-th.icon-field,
vn-th.icon-field *,
vn-td.icon-field,
vn-td.icon-field * {
padding: 0
}
vn-td.icon-field > vn-icon {
margin-left: 3px;
margin-right: 3px;
}
vn-table.scrollable.lg {
height: 736px
}
}

View File

@ -0,0 +1 @@
Sales monitor: Monitor de ventas

View File

@ -0,0 +1,4 @@
<vn-portal slot="menu">
<vn-left-menu></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import ModuleMain from 'salix/components/module-main';
export default class Monitor extends ModuleMain {}
ngModule.vnComponent('vnMonitor', {
controller: Monitor,
template: require('./index.html')
});

View File

@ -0,0 +1,3 @@
import {ng} from 'core/vendor';
export default ng.module('monitor', ['salix']);

View File

@ -0,0 +1,29 @@
{
"module": "monitor",
"name": "Monitors",
"icon" : "grid_view",
"dependencies": ["ticket", "worker", "client"],
"validations" : true,
"menus": {
"main": [
{"state": "monitor.index", "icon": "grid_view"}
],
"card": []
},
"keybindings": [],
"routes": [
{
"url": "/monitor",
"state": "monitor",
"abstract": true,
"component": "vn-monitor",
"description": "Monitors"
},
{
"url": "/index?q",
"state": "monitor.index",
"component": "vn-monitor-index",
"description": "Sales monitor"
}
]
}

View File

@ -23,11 +23,11 @@ describe('order filter()', () => {
}); });
it('should call the filter method with a complex advanced search', async() => { it('should call the filter method with a complex advanced search', async() => {
const filter = {where: {'o.confirmed': false, 'c.salesPersonFk': 19}}; const filter = {where: {'o.confirmed': false, 'c.salesPersonFk': 18}};
const result = await app.models.Order.filter(ctx, filter); const result = await app.models.Order.filter(ctx, filter);
expect(result.length).toEqual(7); expect(result.length).toEqual(9);
expect(result[0].id).toEqual(16); expect(result[0].id).toEqual(7);
}); });
it('should return the orders matching the showEmpty on false', async() => { it('should return the orders matching the showEmpty on false', async() => {

View File

@ -14,7 +14,7 @@
<vn-th field="clientFk">Client</vn-th> <vn-th field="clientFk">Client</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th> <vn-th field="isConfirmed" center>Confirmed</vn-th>
<vn-th field="created" center expand>Created</vn-th> <vn-th field="created" center expand>Created</vn-th>
<vn-th field="landed" default-order="DESC" shrink-date>Landed</vn-th> <vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th field="created" center>Hour</vn-th> <vn-th field="created" center>Hour</vn-th>
<vn-th field="agencyName" center>Agency</vn-th> <vn-th field="agencyName" center>Agency</vn-th>
<vn-th center>Total</vn-th> <vn-th center>Total</vn-th>

View File

@ -89,18 +89,18 @@ describe('ticket filter()', () => {
}); });
it('should return the tickets from the worker team', async() => { it('should return the tickets from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {myTeam: true}}; const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: true}};
const filter = {}; const filter = {};
const result = await app.models.Ticket.filter(ctx, filter); const result = await app.models.Ticket.filter(ctx, filter);
expect(result.length).toEqual(17); expect(result.length).toEqual(20);
}); });
it('should return the tickets that are not from the worker team', async() => { it('should return the tickets that are not from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {myTeam: false}}; const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: false}};
const filter = {}; const filter = {};
const result = await app.models.Ticket.filter(ctx, filter); const result = await app.models.Ticket.filter(ctx, filter);
expect(result.length).toEqual(7); expect(result.length).toEqual(4);
}); });
}); });

View File

@ -20,7 +20,7 @@
ng-model="$ctrl.ticketRequest.attenderFk" ng-model="$ctrl.ticketRequest.attenderFk"
url="Workers/activeWithRole" url="Workers/activeWithRole"
show-field="nickname" show-field="nickname"
where="{role: 'buyer'}" where="{role: {inq: ['logistic', 'buyer']}}"
search-function="{firstName: $search}"> search-function="{firstName: $search}">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -1,7 +1,8 @@
<vn-crud-model auto-load="false" <vn-crud-model
vn-id="model" vn-id="model"
url="Tickets/{{$ctrl.$params.id}}/getSales" url="Tickets/{{$ctrl.$params.id}}/getSales"
data="$ctrl.sales"> data="$ctrl.sales"
auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"

View File

@ -1,9 +1,10 @@
<vn-crud-model auto-load="false" <vn-crud-model
vn-id="model" vn-id="model"
url="Travels/extraCommunityFilter" url="Travels/extraCommunityFilter"
data="travels" data="travels"
order="landed ASC, shipped ASC, travelFk, loadPriority, agencyModeFk, evaNotes" order="landed ASC, shipped ASC, travelFk, loadPriority, agencyModeFk, evaNotes"
limit="20"> limit="20"
auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar

View File

@ -1,7 +1,8 @@
<vn-crud-model auto-load="true" <vn-crud-model
vn-id="model" vn-id="model"
url="Zones/getUpcomingDeliveries" url="Zones/getUpcomingDeliveries"
data="details"> data="details"
auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model"> <vn-data-viewer model="model">
<vn-card> <vn-card>