Merge pull request '2290-client_consumption' (#307) from 2290-client_consumption into dev
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
Reviewed-by: Bernat Exposito <bernat@verdnatura.es>
This commit is contained in:
commit
1b71c1b3b1
|
@ -664,12 +664,12 @@ INSERT INTO `vn`.`itemCategory`(`id`, `name`, `display`, `color`, `icon`, `code`
|
||||||
|
|
||||||
INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`,`workerFk`, `isPackaging`)
|
INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`,`workerFk`, `isPackaging`)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 'CRI', 'Crisantemo', 2, 31, 5, 0),
|
(1, 'CRI', 'Crisantemo', 2, 31, 35, 0),
|
||||||
(2, 'ITG', 'Anthurium', 1, 31, 5, 0),
|
(2, 'ITG', 'Anthurium', 1, 31, 35, 0),
|
||||||
(3, 'WPN', 'Paniculata', 2, 31, 5, 0),
|
(3, 'WPN', 'Paniculata', 2, 31, 35, 0),
|
||||||
(4, 'PRT', 'Delivery ports', 3, NULL, 5, 1),
|
(4, 'PRT', 'Delivery ports', 3, NULL, 35, 1),
|
||||||
(5, 'CON', 'Container', 3, NULL, 5, 1),
|
(5, 'CON', 'Container', 3, NULL, 35, 1),
|
||||||
(6, 'ALS', 'Alstroemeria', 1, 31, 5, 0);
|
(6, 'ALS', 'Alstroemeria', 1, 31, 35, 0);
|
||||||
|
|
||||||
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`)
|
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('./icons/Material-Design-Icons.woff2') format('woff2');
|
src: url('./icons/MaterialIcons-Regular.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,122 @@
|
||||||
|
|
||||||
|
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
||||||
|
const buildFilter = require('vn-loopback/util/filter').buildFilter;
|
||||||
|
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('consumption', {
|
||||||
|
description: 'Find all instances of the model matched by filter from the data source.',
|
||||||
|
accessType: 'READ',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
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 integer searchs by id, otherwise it searchs by name`
|
||||||
|
}, {
|
||||||
|
arg: 'itemFk',
|
||||||
|
type: 'Integer',
|
||||||
|
description: 'Item id'
|
||||||
|
}, {
|
||||||
|
arg: 'categoryFk',
|
||||||
|
type: 'Integer',
|
||||||
|
description: 'Category id'
|
||||||
|
}, {
|
||||||
|
arg: 'typeFk',
|
||||||
|
type: 'Integer',
|
||||||
|
description: 'Item type id',
|
||||||
|
}, {
|
||||||
|
arg: 'buyerFk',
|
||||||
|
type: 'Integer',
|
||||||
|
description: 'Buyer id'
|
||||||
|
}, {
|
||||||
|
arg: 'from',
|
||||||
|
type: 'Date',
|
||||||
|
description: `The from date filter`
|
||||||
|
}, {
|
||||||
|
arg: 'to',
|
||||||
|
type: 'Date',
|
||||||
|
description: `The to date filter`
|
||||||
|
}, {
|
||||||
|
arg: 'grouped',
|
||||||
|
type: 'Boolean',
|
||||||
|
description: 'Group by item'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: {
|
||||||
|
type: ['Object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/consumption`,
|
||||||
|
verb: 'GET'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.consumption = async(ctx, filter) => {
|
||||||
|
const conn = Self.dataSource.connector;
|
||||||
|
const args = ctx.args;
|
||||||
|
const where = buildFilter(ctx.args, (param, value) => {
|
||||||
|
switch (param) {
|
||||||
|
case 'search':
|
||||||
|
return /^\d+$/.test(value)
|
||||||
|
? {'i.id': value}
|
||||||
|
: {'i.name': {like: `%${value}%`}};
|
||||||
|
case 'itemId':
|
||||||
|
return {'i.id': value};
|
||||||
|
case 'description':
|
||||||
|
return {'i.description': {like: `%${value}%`}};
|
||||||
|
case 'categoryId':
|
||||||
|
return {'it.categoryFk': value};
|
||||||
|
case 'typeId':
|
||||||
|
return {'it.id': value};
|
||||||
|
case 'buyerId':
|
||||||
|
return {'it.workerFk': value};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
filter = mergeFilters(filter, {where});
|
||||||
|
|
||||||
|
let stmt = new ParameterizedSQL('SELECT');
|
||||||
|
if (args.grouped)
|
||||||
|
stmt.merge(`SUM(s.quantity) AS quantity,`);
|
||||||
|
else
|
||||||
|
stmt.merge(`s.quantity,`);
|
||||||
|
|
||||||
|
stmt.merge(`s.itemFk,
|
||||||
|
s.concept,
|
||||||
|
s.ticketFk,
|
||||||
|
t.shipped,
|
||||||
|
i.name AS itemName,
|
||||||
|
i.size AS itemSize,
|
||||||
|
i.typeFk AS itemTypeFk,
|
||||||
|
i.subName,
|
||||||
|
i.tag5,
|
||||||
|
i.value5,
|
||||||
|
i.tag6,
|
||||||
|
i.value6,
|
||||||
|
i.tag7,
|
||||||
|
i.value7,
|
||||||
|
i.tag8,
|
||||||
|
i.value8,
|
||||||
|
i.tag9,
|
||||||
|
i.value9,
|
||||||
|
i.tag10,
|
||||||
|
i.value10
|
||||||
|
FROM sale s
|
||||||
|
JOIN ticket t ON t.id = s.ticketFk
|
||||||
|
JOIN item i ON i.id = s.itemFk
|
||||||
|
JOIN itemType it ON it.id = i.typeFk`, [args.grouped]);
|
||||||
|
|
||||||
|
stmt.merge(conn.makeWhere(filter.where));
|
||||||
|
|
||||||
|
if (args.grouped)
|
||||||
|
stmt.merge(`GROUP BY s.itemFk`);
|
||||||
|
|
||||||
|
stmt.merge(conn.makePagination(filter));
|
||||||
|
|
||||||
|
return conn.executeStmt(stmt);
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
|
||||||
|
describe('client consumption() filter', () => {
|
||||||
|
it('should return a list of buyed items by ticket', async() => {
|
||||||
|
const ctx = {req: {accessToken: {userId: 9}}, args: {}};
|
||||||
|
const filter = {
|
||||||
|
where: {
|
||||||
|
clientFk: 101
|
||||||
|
},
|
||||||
|
order: 'itemTypeFk, itemName, itemSize'
|
||||||
|
};
|
||||||
|
const result = await app.models.Client.consumption(ctx, filter);
|
||||||
|
|
||||||
|
expect(result.length).toEqual(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of tickets grouped by item', async() => {
|
||||||
|
const ctx = {req: {accessToken: {userId: 9}},
|
||||||
|
args: {
|
||||||
|
grouped: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const filter = {
|
||||||
|
where: {
|
||||||
|
clientFk: 101
|
||||||
|
},
|
||||||
|
order: 'itemTypeFk, itemName, itemSize'
|
||||||
|
};
|
||||||
|
const result = await app.models.Client.consumption(ctx, filter);
|
||||||
|
|
||||||
|
const firstRow = result[0];
|
||||||
|
const secondRow = result[1];
|
||||||
|
const thirdRow = result[2];
|
||||||
|
|
||||||
|
expect(result.length).toEqual(3);
|
||||||
|
expect(firstRow.quantity).toEqual(10);
|
||||||
|
expect(secondRow.quantity).toEqual(15);
|
||||||
|
expect(thirdRow.quantity).toEqual(20);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,7 +2,6 @@ let request = require('request-promise-native');
|
||||||
let UserError = require('vn-loopback/util/user-error');
|
let UserError = require('vn-loopback/util/user-error');
|
||||||
let getFinalState = require('vn-loopback/util/hook').getFinalState;
|
let getFinalState = require('vn-loopback/util/hook').getFinalState;
|
||||||
let isMultiple = require('vn-loopback/util/hook').isMultiple;
|
let isMultiple = require('vn-loopback/util/hook').isMultiple;
|
||||||
const httpParamSerializer = require('vn-loopback/util/http').httpParamSerializer;
|
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
|
@ -27,6 +26,7 @@ module.exports = Self => {
|
||||||
require('../methods/client/sendSms')(Self);
|
require('../methods/client/sendSms')(Self);
|
||||||
require('../methods/client/createAddress')(Self);
|
require('../methods/client/createAddress')(Self);
|
||||||
require('../methods/client/updateAddress')(Self);
|
require('../methods/client/updateAddress')(Self);
|
||||||
|
require('../methods/client/consumption')(Self);
|
||||||
|
|
||||||
// Validations
|
// Validations
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<div class="search-panel">
|
||||||
|
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
|
||||||
|
<vn-horizontal>
|
||||||
|
<vn-textfield vn-focus
|
||||||
|
vn-one
|
||||||
|
label="General search"
|
||||||
|
ng-model="filter.search"
|
||||||
|
vn-focus>
|
||||||
|
</vn-textfield>
|
||||||
|
</vn-horizontal>
|
||||||
|
<vn-horizontal>
|
||||||
|
<vn-textfield
|
||||||
|
vn-one
|
||||||
|
label="Item id"
|
||||||
|
ng-model="filter.itemId">
|
||||||
|
</vn-textfield>
|
||||||
|
<vn-autocomplete
|
||||||
|
vn-one
|
||||||
|
ng-model="filter.buyerId"
|
||||||
|
url="Clients/activeWorkersWithRole"
|
||||||
|
search-function="{firstName: $search}"
|
||||||
|
value-field="id"
|
||||||
|
where="{role: 'employee'}"
|
||||||
|
label="Buyer">
|
||||||
|
<tpl-item>{{nickname}}</tpl-item>
|
||||||
|
</vn-autocomplete>
|
||||||
|
</vn-horizontal>
|
||||||
|
<vn-horizontal>
|
||||||
|
<vn-autocomplete vn-one
|
||||||
|
ng-model="filter.typeId"
|
||||||
|
url="ItemTypes"
|
||||||
|
show-field="name"
|
||||||
|
value-field="id"
|
||||||
|
label="Type"
|
||||||
|
fields="['categoryFk']"
|
||||||
|
include="'category'">
|
||||||
|
<tpl-item>
|
||||||
|
<div>{{name}}</div>
|
||||||
|
<div class="text-caption text-secondary">
|
||||||
|
{{category.name}}
|
||||||
|
</div>
|
||||||
|
</tpl-item>
|
||||||
|
</vn-autocomplete>
|
||||||
|
<vn-autocomplete vn-one
|
||||||
|
url="ItemCategories"
|
||||||
|
label="Category"
|
||||||
|
show-field="name"
|
||||||
|
value-field="id"
|
||||||
|
ng-model="filter.categoryId">
|
||||||
|
</vn-autocomplete>
|
||||||
|
</vn-horizontal>
|
||||||
|
<vn-horizontal>
|
||||||
|
<vn-date-picker
|
||||||
|
vn-one
|
||||||
|
label="From"
|
||||||
|
ng-model="filter.from">
|
||||||
|
</vn-date-picker>
|
||||||
|
<vn-date-picker
|
||||||
|
vn-one
|
||||||
|
label="To"
|
||||||
|
ng-model="filter.to">
|
||||||
|
</vn-date-picker>
|
||||||
|
</vn-horizontal>
|
||||||
|
<vn-horizontal class="vn-mt-lg">
|
||||||
|
<vn-submit label="Search"></vn-submit>
|
||||||
|
</vn-horizontal>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
import ngModule from '../module';
|
||||||
|
import SearchPanel from 'core/components/searchbar/search-panel';
|
||||||
|
|
||||||
|
ngModule.component('vnConsumptionSearchPanel', {
|
||||||
|
template: require('./index.html'),
|
||||||
|
controller: SearchPanel
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
Item id: Id artículo
|
||||||
|
From: Desde
|
||||||
|
To: Hasta
|
|
@ -0,0 +1,91 @@
|
||||||
|
<vn-crud-model vn-id="model"
|
||||||
|
url="Clients/consumption"
|
||||||
|
link="{clientFk: $ctrl.$params.id}"
|
||||||
|
filter="::$ctrl.filter"
|
||||||
|
user-params="::$ctrl.filterParams"
|
||||||
|
data="sales"
|
||||||
|
order="itemTypeFk, itemName, itemSize">
|
||||||
|
</vn-crud-model>
|
||||||
|
<vn-portal slot="topbar">
|
||||||
|
<vn-searchbar
|
||||||
|
panel="vn-consumption-search-panel"
|
||||||
|
suggested-filter="$ctrl.filterParams"
|
||||||
|
info="Search by item id or name"
|
||||||
|
model="model"
|
||||||
|
auto-state="false">
|
||||||
|
</vn-searchbar>
|
||||||
|
</vn-portal>
|
||||||
|
<vn-data-viewer model="model">
|
||||||
|
<vn-card class="vn-pa-lg vn-w-lg">
|
||||||
|
<section class="header">
|
||||||
|
<vn-tool-bar class="vn-mb-md">
|
||||||
|
<vn-button disabled="!model.userParams.from || !model.userParams.to"
|
||||||
|
icon="picture_as_pdf"
|
||||||
|
ng-click="$ctrl.showReport()"
|
||||||
|
vn-tooltip="Open as PDF">
|
||||||
|
</vn-button>
|
||||||
|
<vn-button disabled="!model.userParams.from || !model.userParams.to"
|
||||||
|
icon="email"
|
||||||
|
ng-click="confirm.show()"
|
||||||
|
vn-tooltip="Send to email">
|
||||||
|
</vn-button>
|
||||||
|
<vn-check
|
||||||
|
label="Group by item"
|
||||||
|
on-change="$ctrl.changeGrouped(value)">
|
||||||
|
</vn-check>
|
||||||
|
</vn-tool-bar>
|
||||||
|
</section>
|
||||||
|
<vn-table model="model">
|
||||||
|
<vn-thead>
|
||||||
|
<vn-tr>
|
||||||
|
<vn-th field="itemFk" number>Item</vn-th>
|
||||||
|
<vn-th field="ticketFk" number>Ticket</vn-th>
|
||||||
|
<vn-th field="shipped">Fecha</vn-th>
|
||||||
|
<vn-th expand>Description</vn-th>
|
||||||
|
<vn-th field="quantity" number>Quantity</vn-th>
|
||||||
|
</vn-tr>
|
||||||
|
</vn-thead>
|
||||||
|
<vn-tbody>
|
||||||
|
<vn-tr
|
||||||
|
ng-repeat="sale in sales">
|
||||||
|
<vn-td number>
|
||||||
|
<span
|
||||||
|
ng-click="itemDescriptor.show($event, sale.itemFk)"
|
||||||
|
class="link">
|
||||||
|
{{::sale.itemFk}}
|
||||||
|
</span>
|
||||||
|
</vn-td>
|
||||||
|
<vn-td number>
|
||||||
|
<span
|
||||||
|
ng-click="ticketDescriptor.show($event, sale.ticketFk)"
|
||||||
|
class="link">
|
||||||
|
{{::sale.ticketFk}}
|
||||||
|
</span>
|
||||||
|
</vn-td>
|
||||||
|
<vn-td>{{::sale.shipped | date: 'dd/MM/yyyy'}}</vn-td>
|
||||||
|
<vn-td expand>
|
||||||
|
<vn-fetched-tags
|
||||||
|
max-length="6"
|
||||||
|
item="::sale"
|
||||||
|
name="::sale.concept"
|
||||||
|
sub-name="::sale.subName">
|
||||||
|
</vn-fetched-tags>
|
||||||
|
</vn-td>
|
||||||
|
<vn-td number>{{::sale.quantity | dashIfEmpty}}</vn-td>
|
||||||
|
</vn-tr>
|
||||||
|
</vn-tbody>
|
||||||
|
</vn-table>
|
||||||
|
</vn-card>
|
||||||
|
</vn-data-viewer>
|
||||||
|
<vn-item-descriptor-popover
|
||||||
|
vn-id="item-descriptor">
|
||||||
|
</vn-item-descriptor-popover>
|
||||||
|
<vn-ticket-descriptor-popover
|
||||||
|
vn-id="ticket-descriptor">
|
||||||
|
</vn-ticket-descriptor-popover>
|
||||||
|
<vn-confirm
|
||||||
|
vn-id="confirm"
|
||||||
|
question="Please, confirm"
|
||||||
|
message="The consumption report will be sent"
|
||||||
|
on-accept="$ctrl.sendEmail()">
|
||||||
|
</vn-confirm>
|
|
@ -0,0 +1,69 @@
|
||||||
|
import ngModule from '../module';
|
||||||
|
import Section from 'salix/components/section';
|
||||||
|
|
||||||
|
class Controller extends Section {
|
||||||
|
constructor($element, $, vnReport, vnEmail) {
|
||||||
|
super($element, $);
|
||||||
|
this.vnReport = vnReport;
|
||||||
|
this.vnEmail = vnEmail;
|
||||||
|
|
||||||
|
this.filter = {
|
||||||
|
where: {
|
||||||
|
isPackaging: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const minDate = new Date();
|
||||||
|
minDate.setHours(0, 0, 0, 0);
|
||||||
|
minDate.setMonth(minDate.getMonth() - 2);
|
||||||
|
|
||||||
|
const maxDate = new Date();
|
||||||
|
maxDate.setHours(23, 59, 59, 59);
|
||||||
|
|
||||||
|
this.filterParams = {
|
||||||
|
from: minDate,
|
||||||
|
to: maxDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get reportParams() {
|
||||||
|
const userParams = this.$.model.userParams;
|
||||||
|
return Object.assign({
|
||||||
|
authorization: this.vnToken.token,
|
||||||
|
recipientId: this.client.id
|
||||||
|
}, userParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
showTicketDescriptor(event, sale) {
|
||||||
|
if (!sale.isTicket) return;
|
||||||
|
|
||||||
|
this.$.ticketDescriptor.show(event.target, sale.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
showReport() {
|
||||||
|
this.vnReport.show('campaign-metrics', this.reportParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmail() {
|
||||||
|
this.vnEmail.send('campaign-metrics', this.reportParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeGrouped(value) {
|
||||||
|
const model = this.$.model;
|
||||||
|
|
||||||
|
model.addFilter({}, {grouped: value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
|
||||||
|
|
||||||
|
ngModule.component('vnClientConsumption', {
|
||||||
|
template: require('./index.html'),
|
||||||
|
controller: Controller,
|
||||||
|
bindings: {
|
||||||
|
client: '<'
|
||||||
|
},
|
||||||
|
require: {
|
||||||
|
card: '^vnClientCard'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
import './index.js';
|
||||||
|
import crudModel from 'core/mocks/crud-model';
|
||||||
|
|
||||||
|
describe('Client', () => {
|
||||||
|
describe('Component vnClientConsumption', () => {
|
||||||
|
let $scope;
|
||||||
|
let controller;
|
||||||
|
let $httpParamSerializer;
|
||||||
|
let $httpBackend;
|
||||||
|
|
||||||
|
beforeEach(ngModule('client'));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpParamSerializer_, _$httpBackend_) => {
|
||||||
|
$scope = $rootScope.$new();
|
||||||
|
$httpParamSerializer = _$httpParamSerializer_;
|
||||||
|
$httpBackend = _$httpBackend_;
|
||||||
|
const $element = angular.element('<vn-client-consumption></vn-client-consumption');
|
||||||
|
controller = $componentController('vnClientConsumption', {$element, $scope});
|
||||||
|
controller.$.model = crudModel;
|
||||||
|
controller.client = {
|
||||||
|
id: 101
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('showReport()', () => {
|
||||||
|
it('should call the window.open function', () => {
|
||||||
|
jest.spyOn(window, 'open').mockReturnThis();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
controller.$.model.userParams = {
|
||||||
|
from: now,
|
||||||
|
to: now
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.showReport();
|
||||||
|
|
||||||
|
const expectedParams = {
|
||||||
|
recipientId: 101,
|
||||||
|
from: now,
|
||||||
|
to: now
|
||||||
|
};
|
||||||
|
const serializedParams = $httpParamSerializer(expectedParams);
|
||||||
|
const path = `api/report/campaign-metrics?${serializedParams}`;
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith(path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendEmail()', () => {
|
||||||
|
it('should make a GET query sending the report', () => {
|
||||||
|
const now = new Date();
|
||||||
|
controller.$.model.userParams = {
|
||||||
|
from: now,
|
||||||
|
to: now
|
||||||
|
};
|
||||||
|
const expectedParams = {
|
||||||
|
recipientId: 101,
|
||||||
|
from: now,
|
||||||
|
to: now
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializedParams = $httpParamSerializer(expectedParams);
|
||||||
|
const path = `email/campaign-metrics?${serializedParams}`;
|
||||||
|
|
||||||
|
$httpBackend.expect('GET', path).respond({});
|
||||||
|
controller.sendEmail();
|
||||||
|
$httpBackend.flush();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Group by item: Agrupar por artículo
|
||||||
|
Open as PDF: Abrir como PDF
|
||||||
|
Send to email: Enviar por email
|
||||||
|
Search by item id or name: Buscar por id de artículo o nombre
|
||||||
|
The consumption report will be sent: Se enviará el informe de consumo
|
||||||
|
Please, confirm: Por favor, confirma
|
|
@ -13,11 +13,6 @@
|
||||||
translate>
|
translate>
|
||||||
Send SMS
|
Send SMS
|
||||||
</vn-item>
|
</vn-item>
|
||||||
<vn-item
|
|
||||||
ng-click="consumerReportDialog.show()"
|
|
||||||
translate>
|
|
||||||
View consumer report
|
|
||||||
</vn-item>
|
|
||||||
</slot-menu>
|
</slot-menu>
|
||||||
<slot-body>
|
<slot-body>
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
|
@ -94,28 +89,4 @@
|
||||||
<vn-client-sms
|
<vn-client-sms
|
||||||
vn-id="sms"
|
vn-id="sms"
|
||||||
sms="$ctrl.newSMS">
|
sms="$ctrl.newSMS">
|
||||||
</vn-client-sms>
|
</vn-client-sms>
|
||||||
<vn-dialog
|
|
||||||
vn-id="consumerReportDialog"
|
|
||||||
on-accept="$ctrl.onConsumerReportAccept()"
|
|
||||||
message="Send consumer report">
|
|
||||||
<tpl-body>
|
|
||||||
<vn-date-picker
|
|
||||||
vn-id="from"
|
|
||||||
vn-one
|
|
||||||
ng-model="$ctrl.from"
|
|
||||||
label="From date"
|
|
||||||
vn-focus>
|
|
||||||
</vn-date-picker>
|
|
||||||
<vn-date-picker
|
|
||||||
vn-id="to"
|
|
||||||
vn-one
|
|
||||||
ng-model="$ctrl.to"
|
|
||||||
label="To date">
|
|
||||||
</vn-date-picker>
|
|
||||||
</tpl-body>
|
|
||||||
<tpl-buttons>
|
|
||||||
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
|
|
||||||
<button response="accept" translate>Accept</button>
|
|
||||||
</tpl-buttons>
|
|
||||||
</vn-dialog>
|
|
|
@ -39,14 +39,6 @@ class Controller extends Descriptor {
|
||||||
};
|
};
|
||||||
this.$.sms.open();
|
this.$.sms.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
onConsumerReportAccept() {
|
|
||||||
this.vnReport.show('campaign-metrics', {
|
|
||||||
recipientId: this.id,
|
|
||||||
from: this.from,
|
|
||||||
to: this.to,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngModule.vnComponent('vnClientDescriptor', {
|
ngModule.vnComponent('vnClientDescriptor', {
|
||||||
|
|
|
@ -40,3 +40,5 @@ import './postcode';
|
||||||
import './dms/index';
|
import './dms/index';
|
||||||
import './dms/create';
|
import './dms/create';
|
||||||
import './dms/edit';
|
import './dms/edit';
|
||||||
|
import './consumption';
|
||||||
|
import './consumption-search-panel';
|
||||||
|
|
|
@ -56,4 +56,5 @@ Requested credits: Créditos solicitados
|
||||||
Contacts: Contactos
|
Contacts: Contactos
|
||||||
Samples: Plantillas
|
Samples: Plantillas
|
||||||
Send sample: Enviar plantilla
|
Send sample: Enviar plantilla
|
||||||
Log: Historial
|
Log: Historial
|
||||||
|
Consumption: Consumo
|
|
@ -18,16 +18,17 @@
|
||||||
{"state": "client.card.greuge.index", "icon": "work"},
|
{"state": "client.card.greuge.index", "icon": "work"},
|
||||||
{"state": "client.card.balance.index", "icon": "icon-invoices"},
|
{"state": "client.card.balance.index", "icon": "icon-invoices"},
|
||||||
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
|
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
|
||||||
|
{"state": "client.card.webAccess", "icon": "cloud"},
|
||||||
{"state": "client.card.log", "icon": "history"},
|
{"state": "client.card.log", "icon": "history"},
|
||||||
{
|
{
|
||||||
"description": "Others",
|
"description": "Others",
|
||||||
"icon": "more",
|
"icon": "more",
|
||||||
"childs": [
|
"childs": [
|
||||||
{"state": "client.card.webAccess", "icon": "cloud"},
|
{"state": "client.card.sample.index", "icon": "mail"},
|
||||||
|
{"state": "client.card.consumption", "icon": "show_chart"},
|
||||||
{"state": "client.card.mandate", "icon": "pan_tool"},
|
{"state": "client.card.mandate", "icon": "pan_tool"},
|
||||||
{"state": "client.card.creditInsurance.index", "icon": "icon-solunion"},
|
{"state": "client.card.creditInsurance.index", "icon": "icon-solunion"},
|
||||||
{"state": "client.card.contact", "icon": "contact_phone"},
|
{"state": "client.card.contact", "icon": "contact_phone"},
|
||||||
{"state": "client.card.sample.index", "icon": "mail"},
|
|
||||||
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
|
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
|
||||||
{"state": "client.card.dms.index", "icon": "cloud_upload"}
|
{"state": "client.card.dms.index", "icon": "cloud_upload"}
|
||||||
]
|
]
|
||||||
|
@ -350,6 +351,15 @@
|
||||||
"params": {
|
"params": {
|
||||||
"client": "$ctrl.client"
|
"client": "$ctrl.client"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/consumption",
|
||||||
|
"state": "client.card.consumption",
|
||||||
|
"component": "vn-client-consumption",
|
||||||
|
"description": "Consumption",
|
||||||
|
"params": {
|
||||||
|
"client": "$ctrl.client"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.privacy {
|
.privacy {
|
||||||
|
text-align: center;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 100
|
font-weight: 100
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
header {
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
header .logo {
|
header .logo {
|
||||||
margin-bottom: 15px;
|
margin-top: 25px;
|
||||||
|
margin-bottom: 25px
|
||||||
}
|
}
|
||||||
|
|
||||||
header .logo img {
|
header .logo img {
|
||||||
|
|
|
@ -2,5 +2,6 @@ const Vue = require('vue');
|
||||||
const strftime = require('strftime');
|
const strftime = require('strftime');
|
||||||
|
|
||||||
Vue.filter('date', function(value, specifiers = '%d-%m-%Y') {
|
Vue.filter('date', function(value, specifiers = '%d-%m-%Y') {
|
||||||
|
if (!(value instanceof Date)) value = new Date(value);
|
||||||
return strftime(specifiers, value);
|
return strftime(specifiers, value);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"filename": "campaing-metrics",
|
"filename": "campaign-metrics.pdf",
|
||||||
"component": "campaign-metrics"
|
"component": "campaign-metrics"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -25,7 +25,7 @@
|
||||||
<div class="grid-block vn-pa-lg">
|
<div class="grid-block vn-pa-lg">
|
||||||
<h1>{{ $t('title') }}</h1>
|
<h1>{{ $t('title') }}</h1>
|
||||||
<p>{{$t('dear')}},</p>
|
<p>{{$t('dear')}},</p>
|
||||||
<p>{{$t('description')}}</p>
|
<p v-html="$t('description', [minDate, maxDate])"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Footer block -->
|
<!-- Footer block -->
|
||||||
|
|
|
@ -4,7 +4,17 @@ const emailFooter = new Component('email-footer');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'campaign-metrics',
|
name: 'campaign-metrics',
|
||||||
|
created() {
|
||||||
|
this.filters = this.$options.filters;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
minDate: function() {
|
||||||
|
return this.filters.date(this.from, '%d-%m-%Y');
|
||||||
|
},
|
||||||
|
maxDate: function() {
|
||||||
|
return this.filters.date(this.to, '%d-%m-%Y');
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
'email-header': emailHeader.build(),
|
'email-header': emailHeader.build(),
|
||||||
'email-footer': emailFooter.build()
|
'email-footer': emailFooter.build()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
subject: Informe consumo campaña
|
subject: Informe de consumo
|
||||||
title: Informe consumo campaña
|
title: Informe de consumo
|
||||||
dear: Estimado cliente
|
dear: Estimado cliente
|
||||||
description: Con motivo de esta próxima campaña, me complace
|
description: Tal y como nos ha solicitado nos complace
|
||||||
relacionarle a continuación el consumo que nos consta en su cuenta para las
|
relacionarle a continuación el consumo que nos consta en su cuenta para las
|
||||||
mismas fechas del año pasado. Espero le sea de utilidad para preparar su pedido.
|
fechas comprendidas entre <strong>{0}</strong> y <strong>{1}</strong>.
|
||||||
|
Espero le sea de utilidad para preparar su pedido.<br/><br/>
|
||||||
Al mismo tiempo aprovecho la ocasión para saludarle cordialmente.
|
Al mismo tiempo aprovecho la ocasión para saludarle cordialmente.
|
||||||
|
|
|
@ -6,9 +6,6 @@ const reportFooter = new Component('report-footer');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'campaign-metrics',
|
name: 'campaign-metrics',
|
||||||
async serverPrefetch() {
|
async serverPrefetch() {
|
||||||
this.to = new Date(this.to);
|
|
||||||
this.from = new Date(this.from);
|
|
||||||
|
|
||||||
this.client = await this.fetchClient(this.recipientId);
|
this.client = await this.fetchClient(this.recipientId);
|
||||||
this.sales = await this.fetchSales(this.recipientId, this.from, this.to);
|
this.sales = await this.fetchSales(this.recipientId, this.from, this.to);
|
||||||
|
|
||||||
|
@ -54,7 +51,7 @@ module.exports = {
|
||||||
t.clientFk = ? AND it.isPackaging = FALSE
|
t.clientFk = ? AND it.isPackaging = FALSE
|
||||||
AND DATE(t.shipped) BETWEEN ? AND ?
|
AND DATE(t.shipped) BETWEEN ? AND ?
|
||||||
GROUP BY s.itemFk
|
GROUP BY s.itemFk
|
||||||
ORDER BY i.typeFk , i.name , i.size`, [clientId, from, to]);
|
ORDER BY i.typeFk , i.name`, [clientId, from, to]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -66,12 +63,10 @@ module.exports = {
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
required: true,
|
required: true
|
||||||
type: Date
|
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
required: true,
|
required: true
|
||||||
type: Date
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
title: Consumo de campaña
|
title: Consumo
|
||||||
Client: Cliente
|
Client: Cliente
|
||||||
clientData: Datos del cliente
|
clientData: Datos del cliente
|
||||||
dated: Fecha
|
dated: Fecha
|
||||||
|
|
Loading…
Reference in New Issue