Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2896-route_tickets_refactor
This commit is contained in:
commit
2b83abeefd
|
@ -1,2 +1,4 @@
|
|||
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId)
|
||||
VALUES ('SupplierAddress', '*', '*', 'ALLOW', 'ROLE', 'employee');
|
||||
VALUES
|
||||
('SupplierAddress', '*', '*', 'ALLOW', 'ROLE', 'employee'),
|
||||
('SalesMonitor', '*', '*', 'ALLOW', 'ROLE', 'employee');
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Delete me
|
|
@ -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),
|
||||
(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),
|
||||
(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),
|
||||
(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);
|
||||
|
@ -1560,6 +1560,66 @@ INSERT INTO `hedera`.`orderRowComponent`(`rowFk`, `componentFk`, `price`)
|
|||
(30, 37, 2),
|
||||
(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`)
|
||||
VALUES
|
||||
(1, 101, 'contact 1', 666777888),
|
||||
|
@ -1689,11 +1749,12 @@ INSERT INTO `vn`.`receipt`(`id`, `invoiceFk`, `amountPaid`, `amountUnpaid`, `pay
|
|||
INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`)
|
||||
VALUES
|
||||
(1, 1, 9),
|
||||
(2, 1, 18),
|
||||
(3, 2, 101),
|
||||
(4, 2, 102),
|
||||
(5, 3, 103),
|
||||
(6, 3, 104);
|
||||
(2, 2, 18),
|
||||
(3, 2, 19),
|
||||
(4, 3, 101),
|
||||
(5, 3, 102),
|
||||
(6, 4, 103),
|
||||
(7, 4, 104);
|
||||
|
||||
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `attenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
|
||||
VALUES
|
||||
|
|
|
@ -36803,10 +36803,6 @@ BEGIN
|
|||
NEW.id);
|
||||
END IF;
|
||||
|
||||
IF !(DATE(NEW.shipped) <=> DATE(OLD.shipped)) AND DATE(NEW.shipped) = CURDATE() THEN
|
||||
INSERT INTO tmp.ticketDate_updated(ticketFk, oldShipped, newShipped, workerFk)
|
||||
VALUES (NEW.id, OLD.shipped, NEW.shipped, vn.getUser());
|
||||
END IF;
|
||||
END */;;
|
||||
DELIMITER ;
|
||||
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
||||
|
|
|
@ -123,4 +123,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.small {
|
||||
height: 24px;
|
||||
font-size: .75rem;
|
||||
|
||||
& > button {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,8 @@ export default class Table {
|
|||
this.model.refresh();
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
if (this.model && !this.model.data)
|
||||
this.applyOrder();
|
||||
isScrollable() {
|
||||
return this.table.classList.contains('scrollable');
|
||||
}
|
||||
|
||||
setActiveArrow() {
|
||||
|
|
|
@ -20,7 +20,6 @@ vn-table {
|
|||
font-weight: normal;
|
||||
}
|
||||
& > * > vn-th[field] {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
cursor: pointer
|
||||
}
|
||||
|
@ -83,6 +82,11 @@ vn-table {
|
|||
}
|
||||
&[shrink-date] {
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
&[shrink-datetime] {
|
||||
width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
&[expand] {
|
||||
max-width: 400px;
|
||||
|
@ -183,3 +187,35 @@ vn-table {
|
|||
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
|
||||
}
|
|
@ -14,8 +14,11 @@ export default class Th {
|
|||
$onInit() {
|
||||
if (!this.field) return;
|
||||
|
||||
if (this.defaultOrder)
|
||||
if (this.defaultOrder) {
|
||||
this.order = this.defaultOrder;
|
||||
this.table.applyOrder(this.field, this.order);
|
||||
this.updateArrow();
|
||||
}
|
||||
|
||||
this.updateArrow();
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ export default function moduleImport(moduleName) {
|
|||
case 'entry' : return import('entry/front');
|
||||
case 'account' : return import('account/front');
|
||||
case 'supplier' : return import('supplier/front');
|
||||
case 'monitor' : return import('monitor/front');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ Invoices in: Fact. recibidas
|
|||
Entries: Entradas
|
||||
Users: Usuarios
|
||||
Suppliers: Proveedores
|
||||
Monitors: Monitores
|
||||
|
||||
# Common
|
||||
|
||||
|
|
|
@ -96,5 +96,6 @@
|
|||
"Swift / BIC cannot be empty": "Swift / BIC cannot be empty",
|
||||
"Role name must be written in camelCase": "Role name must be written in camelCase",
|
||||
"Client assignment has changed": "I did change the salesperson ~*\"<{{previousWorkerName}}>\"*~ by *\"<{{currentWorkerName}}>\"* from the client [{{clientName}} ({{clientId}})]({{{url}}})",
|
||||
"None": "None"
|
||||
"None": "None",
|
||||
"error densidad = 0": "error densidad = 0"
|
||||
}
|
|
@ -83,12 +83,11 @@ module.exports = function(Self) {
|
|||
throw new UserError('Invalid account');
|
||||
|
||||
await Self.rawSql(
|
||||
`CALL vn.ledger_doCompensation(?, ?, ?, ?, ?, ?, ?)`,
|
||||
`CALL vn.ledger_doCompensation(CURDATE(), ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
Date(),
|
||||
args.compensationAccount,
|
||||
args.bankFk,
|
||||
accountingType.receiptDescription + args.compensationAccount,
|
||||
accountingType.receiptDescription + clientOriginal.accountingAccount,
|
||||
args.amountPaid,
|
||||
args.companyFk,
|
||||
clientOriginal.accountingAccount
|
||||
|
|
|
@ -234,7 +234,7 @@
|
|||
value="{{$ctrl.summary.mana.mana | currency: 'EUR':2}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Rate"
|
||||
value="{{$ctrl.claimRate($ctrl.summary.claimsRatio.priceIncreasing) | percentage}}">
|
||||
value="{{$ctrl.claimRate($ctrl.summary.claimsRatio.priceIncreasing / 100) | percentage}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Average invoiced"
|
||||
value="{{$ctrl.summary.averageInvoiced.invoiced | currency: 'EUR':2}}">
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
vn-id="model"
|
||||
url="Buys/latestBuysFilter"
|
||||
limit="20"
|
||||
data="$ctrl.buys">
|
||||
data="$ctrl.buys"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
|
|
|
@ -66,6 +66,13 @@ describe('Entry', () => {
|
|||
|
||||
describe('onEditAccept()', () => {
|
||||
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'};
|
||||
let query = 'Buys/editLatestBuys';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"menus": {
|
||||
"main": [
|
||||
{"state": "entry.index", "icon": "icon-entry"},
|
||||
{"state": "entry.latestBuys", "icon": "icon-latestBuys"}
|
||||
{"state": "entry.latestBuys", "icon": "contact_support"}
|
||||
],
|
||||
"card": [
|
||||
{"state": "entry.card.basicData", "icon": "settings"},
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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.makePagination(filter));
|
||||
|
||||
return conn.executeStmt(stmt);
|
||||
};
|
||||
};
|
|
@ -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];
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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: 'date_make DESC'};
|
||||
const result = await app.models.SalesMonitor.ordersFilter(ctx, filter);
|
||||
|
||||
expect(result.length).toEqual(12);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"SalesMonitor": {
|
||||
"dataSource": "vn"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "SalesMonitor",
|
||||
"base": "VnModel",
|
||||
"acls": [{
|
||||
"property": "status",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export * from './module';
|
||||
|
||||
import './main';
|
||||
import './index/';
|
||||
import './index/tickets';
|
||||
import './index/clients';
|
||||
import './index/orders';
|
||||
import './index/search-panel';
|
|
@ -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>
|
|
@ -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
|
||||
});
|
|
@ -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>
|
|
@ -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
|
||||
});
|
|
@ -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
|
|
@ -0,0 +1,85 @@
|
|||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="SalesMonitors/ordersFilter"
|
||||
limit="6"
|
||||
order="date_make 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_make" 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>
|
|
@ -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
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Sales monitor: Monitor de ventas
|
|
@ -0,0 +1,4 @@
|
|||
<vn-portal slot="menu">
|
||||
<vn-left-menu></vn-left-menu>
|
||||
</vn-portal>
|
||||
<ui-view></ui-view>
|
|
@ -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')
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import {ng} from 'core/vendor';
|
||||
|
||||
export default ng.module('monitor', ['salix']);
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -118,8 +118,8 @@ module.exports = Self => {
|
|||
FROM tmp.ticketCalculateItem tci
|
||||
JOIN vn.item i ON i.id = tci.itemFk
|
||||
JOIN vn.itemType it ON it.id = i.typeFk
|
||||
JOIN vn.ink ON ink.id = i.inkFk
|
||||
JOIN vn.worker w on w.id = it.workerFk`);
|
||||
JOIN vn.worker w on w.id = it.workerFk
|
||||
LEFT JOIN vn.ink ON ink.id = i.inkFk`);
|
||||
|
||||
// Apply order by tag
|
||||
if (orderBy.isTag) {
|
||||
|
|
|
@ -23,11 +23,11 @@ describe('order filter()', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(result.length).toEqual(7);
|
||||
expect(result[0].id).toEqual(16);
|
||||
expect(result.length).toEqual(9);
|
||||
expect(result[0].id).toEqual(7);
|
||||
});
|
||||
|
||||
it('should return the orders matching the showEmpty on false', async() => {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<vn-th field="clientFk">Client</vn-th>
|
||||
<vn-th field="isConfirmed" center>Confirmed</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="agencyName" center>Agency</vn-th>
|
||||
<vn-th center>Total</vn-th>
|
||||
|
|
|
@ -20,12 +20,6 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.getSuggestedTickets = async id => {
|
||||
const ticketsInRoute = await Self.app.models.Ticket.find({
|
||||
where: {routeFk: id},
|
||||
fields: ['id']
|
||||
});
|
||||
const idsToExclude = ticketsInRoute.map(ticket => ticket.id);
|
||||
|
||||
const route = await Self.app.models.Route.findById(id);
|
||||
|
||||
const zoneAgencyModes = await Self.app.models.ZoneAgencyMode.find({
|
||||
|
@ -37,19 +31,17 @@ module.exports = Self => {
|
|||
const zoneIds = [];
|
||||
for (let zoneAgencyMode of zoneAgencyModes)
|
||||
zoneIds.push(zoneAgencyMode.zoneFk);
|
||||
|
||||
const minDate = new Date(route.created);
|
||||
minDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const maxDate = new Date(route.created);
|
||||
maxDate.setHours(23, 59, 59, 59);
|
||||
|
||||
let tickets = await Self.app.models.Ticket.find({
|
||||
where: {
|
||||
agencyModeFk: route.agencyModeFk,
|
||||
zoneFk: {inq: zoneIds},
|
||||
id: {nin: idsToExclude},
|
||||
created: {between: [minDate, maxDate]}
|
||||
routeFk: null,
|
||||
shipped: {between: [minDate, maxDate]}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
|
|
|
@ -21,9 +21,14 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.restore = async(ctx, id) => {
|
||||
Self.restore = async(ctx, id, options) => {
|
||||
const models = Self.app.models;
|
||||
const $t = ctx.req.__; // $translate
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const ticket = await models.Ticket.findById(id, {
|
||||
include: [{
|
||||
relation: 'client',
|
||||
|
@ -31,7 +36,7 @@ module.exports = Self => {
|
|||
fields: ['id', 'salesPersonFk']
|
||||
}
|
||||
}]
|
||||
});
|
||||
}, myOptions);
|
||||
|
||||
const now = new Date();
|
||||
const maxDate = new Date(ticket.updated);
|
||||
|
@ -51,6 +56,16 @@ module.exports = Self => {
|
|||
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
|
||||
}
|
||||
|
||||
return ticket.updateAttribute('isDeleted', false);
|
||||
const fullYear = new Date().getFullYear();
|
||||
const newShipped = ticket.shipped;
|
||||
const newLanded = ticket.landed;
|
||||
newShipped.setFullYear(fullYear);
|
||||
newLanded.setFullYear(fullYear);
|
||||
|
||||
return ticket.updateAttributes({
|
||||
shipped: newShipped,
|
||||
landed: newLanded,
|
||||
isDeleted: false
|
||||
}, myOptions);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -89,18 +89,18 @@ describe('ticket filter()', () => {
|
|||
});
|
||||
|
||||
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 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() => {
|
||||
const ctx = {req: {accessToken: {userId: 9}}, args: {myTeam: false}};
|
||||
const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: false}};
|
||||
const filter = {};
|
||||
const result = await app.models.Ticket.filter(ctx, filter);
|
||||
|
||||
expect(result.length).toEqual(7);
|
||||
expect(result.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ const models = app.models;
|
|||
|
||||
describe('ticket restore()', () => {
|
||||
const employeeUser = 110;
|
||||
const ticketId = 18;
|
||||
const activeCtx = {
|
||||
accessToken: {userId: employeeUser},
|
||||
headers: {
|
||||
|
@ -13,45 +14,30 @@ describe('ticket restore()', () => {
|
|||
};
|
||||
const ctx = {req: activeCtx};
|
||||
|
||||
let createdTicket;
|
||||
|
||||
beforeEach(async done => {
|
||||
beforeEach(() => {
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
try {
|
||||
const sampleTicket = await models.Ticket.findById(11);
|
||||
sampleTicket.id = undefined;
|
||||
|
||||
createdTicket = await models.Ticket.create(sampleTicket);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(async done => {
|
||||
try {
|
||||
await models.Ticket.destroyById(createdTicket.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw an error if the given ticket has past the deletion time', async() => {
|
||||
let error;
|
||||
|
||||
const tx = await app.models.Ticket.beginTransaction({});
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() - 1);
|
||||
|
||||
try {
|
||||
const ticket = await models.Ticket.findById(createdTicket.id);
|
||||
await ticket.updateAttributes({isDeleted: true, updated: now});
|
||||
await app.models.Ticket.restore(ctx, createdTicket.id);
|
||||
const options = {transaction: tx};
|
||||
const ticket = await models.Ticket.findById(ticketId, null, options);
|
||||
await ticket.updateAttributes({
|
||||
isDeleted: true,
|
||||
updated: now
|
||||
}, options);
|
||||
await app.models.Ticket.restore(ctx, ticketId, options);
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
error = e;
|
||||
}
|
||||
|
||||
|
@ -59,17 +45,37 @@ describe('ticket restore()', () => {
|
|||
});
|
||||
|
||||
it('should restore the ticket making its state no longer deleted', async() => {
|
||||
const tx = await app.models.Ticket.beginTransaction({});
|
||||
const now = new Date();
|
||||
const ticketBeforeUpdate = await models.Ticket.findById(createdTicket.id);
|
||||
await ticketBeforeUpdate.updateAttributes({isDeleted: true, updated: now});
|
||||
|
||||
const ticketAfterUpdate = await models.Ticket.findById(createdTicket.id);
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
const ticketBeforeUpdate = await models.Ticket.findById(ticketId, null, options);
|
||||
await ticketBeforeUpdate.updateAttributes({
|
||||
isDeleted: true,
|
||||
updated: now
|
||||
}, options);
|
||||
|
||||
const ticketAfterUpdate = await models.Ticket.findById(ticketId, null, options);
|
||||
|
||||
expect(ticketAfterUpdate.isDeleted).toBeTruthy();
|
||||
|
||||
await models.Ticket.restore(ctx, createdTicket.id);
|
||||
const ticketAfterRestore = await models.Ticket.findById(createdTicket.id);
|
||||
await models.Ticket.restore(ctx, ticketId, options);
|
||||
const ticketAfterRestore = await models.Ticket.findById(ticketId, null, options);
|
||||
|
||||
const fullYear = now.getFullYear();
|
||||
const shippedFullYear = ticketAfterRestore.shipped.getFullYear();
|
||||
const landedFullYear = ticketAfterRestore.landed.getFullYear();
|
||||
|
||||
expect(ticketAfterRestore.isDeleted).toBeFalsy();
|
||||
expect(shippedFullYear).toEqual(fullYear);
|
||||
expect(landedFullYear).toEqual(fullYear);
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
ng-model="$ctrl.ticketRequest.attenderFk"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
where="{role: 'buyer'}"
|
||||
where="{role: {inq: ['logistic', 'buyer']}}"
|
||||
search-function="{firstName: $search}">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<vn-crud-model auto-load="false"
|
||||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="Tickets/{{$ctrl.$params.id}}/getSales"
|
||||
data="$ctrl.sales">
|
||||
data="$ctrl.sales"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-watcher
|
||||
vn-id="watcher"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<vn-crud-model auto-load="false"
|
||||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="Travels/extraCommunityFilter"
|
||||
data="travels"
|
||||
order="landed ASC, shipped ASC, travelFk, loadPriority, agencyModeFk, evaNotes"
|
||||
limit="20">
|
||||
limit="20"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<vn-crud-model auto-load="true"
|
||||
<vn-crud-model
|
||||
vn-id="model"
|
||||
url="Zones/getUpcomingDeliveries"
|
||||
data="details">
|
||||
data="details"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-data-viewer model="model">
|
||||
<vn-card>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
buttons:
|
||||
webAcccess: Visita nuestra Web
|
||||
info: Ayúdanos a mejorar
|
||||
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla
|
||||
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
|
||||
· www.verdnatura.es · clientes@verdnatura.es
|
||||
disclaimer: '- AVISO - Este mensaje es privado y confidencial, y debe ser utilizado
|
||||
exclusivamente por la persona destinataria del mismo. Si has recibido este mensaje
|
||||
|
|
Loading…
Reference in New Issue