diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 13ac2bcb3..e48a20ec6 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -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`) VALUES - (1, 'CRI', 'Crisantemo', 2, 31, 5, 0), - (2, 'ITG', 'Anthurium', 1, 31, 5, 0), - (3, 'WPN', 'Paniculata', 2, 31, 5, 0), - (4, 'PRT', 'Delivery ports', 3, NULL, 5, 1), - (5, 'CON', 'Container', 3, NULL, 5, 1), - (6, 'ALS', 'Alstroemeria', 1, 31, 5, 0); + (1, 'CRI', 'Crisantemo', 2, 31, 35, 0), + (2, 'ITG', 'Anthurium', 1, 31, 35, 0), + (3, 'WPN', 'Paniculata', 2, 31, 35, 0), + (4, 'PRT', 'Delivery ports', 3, NULL, 35, 1), + (5, 'CON', 'Container', 3, NULL, 35, 1), + (6, 'ALS', 'Alstroemeria', 1, 31, 35, 0); INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`) VALUES diff --git a/front/core/styles/font-family.scss b/front/core/styles/font-family.scss index 035d96bc9..24d9bbe43 100644 --- a/front/core/styles/font-family.scss +++ b/front/core/styles/font-family.scss @@ -14,7 +14,7 @@ font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url('./icons/Material-Design-Icons.woff2') format('woff2'); + src: url('./icons/MaterialIcons-Regular.woff2') format('woff2'); } .material-icons { diff --git a/front/core/styles/icons/Material-Design-Icons.woff2 b/front/core/styles/icons/Material-Design-Icons.woff2 deleted file mode 100644 index 20f1f6746..000000000 Binary files a/front/core/styles/icons/Material-Design-Icons.woff2 and /dev/null differ diff --git a/front/core/styles/icons/MaterialIcons-Regular.woff2 b/front/core/styles/icons/MaterialIcons-Regular.woff2 new file mode 100644 index 000000000..9fa211252 Binary files /dev/null and b/front/core/styles/icons/MaterialIcons-Regular.woff2 differ diff --git a/modules/client/back/methods/client/consumption.js b/modules/client/back/methods/client/consumption.js new file mode 100644 index 000000000..cb5b39c29 --- /dev/null +++ b/modules/client/back/methods/client/consumption.js @@ -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); + }; +}; diff --git a/modules/client/back/methods/client/specs/consumption.spec.js b/modules/client/back/methods/client/specs/consumption.spec.js new file mode 100644 index 000000000..e7a42a8da --- /dev/null +++ b/modules/client/back/methods/client/specs/consumption.spec.js @@ -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); + }); +}); diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 8ceff0c2a..056b49d01 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -2,7 +2,6 @@ let request = require('request-promise-native'); let UserError = require('vn-loopback/util/user-error'); let getFinalState = require('vn-loopback/util/hook').getFinalState; let isMultiple = require('vn-loopback/util/hook').isMultiple; -const httpParamSerializer = require('vn-loopback/util/http').httpParamSerializer; const LoopBackContext = require('loopback-context'); module.exports = Self => { @@ -27,6 +26,7 @@ module.exports = Self => { require('../methods/client/sendSms')(Self); require('../methods/client/createAddress')(Self); require('../methods/client/updateAddress')(Self); + require('../methods/client/consumption')(Self); // Validations diff --git a/modules/client/front/consumption-search-panel/index.html b/modules/client/front/consumption-search-panel/index.html new file mode 100644 index 000000000..e957c891b --- /dev/null +++ b/modules/client/front/consumption-search-panel/index.html @@ -0,0 +1,68 @@ +
+
+ + + + + + + + + {{nickname}} + + + + + +
{{name}}
+
+ {{category.name}} +
+
+
+ + +
+ + + + + + + + + +
+
diff --git a/modules/client/front/consumption-search-panel/index.js b/modules/client/front/consumption-search-panel/index.js new file mode 100644 index 000000000..9b9354a93 --- /dev/null +++ b/modules/client/front/consumption-search-panel/index.js @@ -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 +}); diff --git a/modules/client/front/consumption-search-panel/locale/es.yml b/modules/client/front/consumption-search-panel/locale/es.yml new file mode 100644 index 000000000..68de42b23 --- /dev/null +++ b/modules/client/front/consumption-search-panel/locale/es.yml @@ -0,0 +1,3 @@ +Item id: Id artículo +From: Desde +To: Hasta \ No newline at end of file diff --git a/modules/client/front/consumption/index.html b/modules/client/front/consumption/index.html new file mode 100644 index 000000000..8ea65ecae --- /dev/null +++ b/modules/client/front/consumption/index.html @@ -0,0 +1,91 @@ + + + + + + + + +
+ + + + + + + + +
+ + + + Item + Ticket + Fecha + Description + Quantity + + + + + + + {{::sale.itemFk}} + + + + + {{::sale.ticketFk}} + + + {{::sale.shipped | date: 'dd/MM/yyyy'}} + + + + + {{::sale.quantity | dashIfEmpty}} + + + +
+
+ + + + + + diff --git a/modules/client/front/consumption/index.js b/modules/client/front/consumption/index.js new file mode 100644 index 000000000..4b075abb9 --- /dev/null +++ b/modules/client/front/consumption/index.js @@ -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' + } +}); diff --git a/modules/client/front/consumption/index.spec.js b/modules/client/front/consumption/index.spec.js new file mode 100644 index 000000000..d76bc1e76 --- /dev/null +++ b/modules/client/front/consumption/index.spec.js @@ -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(' { + 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(); + }); + }); + }); +}); + diff --git a/modules/client/front/consumption/locale/es.yml b/modules/client/front/consumption/locale/es.yml new file mode 100644 index 000000000..adf0f060c --- /dev/null +++ b/modules/client/front/consumption/locale/es.yml @@ -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 diff --git a/modules/client/front/descriptor/index.html b/modules/client/front/descriptor/index.html index 19fcba128..aac5eb343 100644 --- a/modules/client/front/descriptor/index.html +++ b/modules/client/front/descriptor/index.html @@ -13,11 +13,6 @@ translate> Send SMS - - View consumer report -
@@ -94,28 +89,4 @@ - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/modules/client/front/descriptor/index.js b/modules/client/front/descriptor/index.js index 7d3f628b4..89248ebed 100644 --- a/modules/client/front/descriptor/index.js +++ b/modules/client/front/descriptor/index.js @@ -39,14 +39,6 @@ class Controller extends Descriptor { }; this.$.sms.open(); } - - onConsumerReportAccept() { - this.vnReport.show('campaign-metrics', { - recipientId: this.id, - from: this.from, - to: this.to, - }); - } } ngModule.vnComponent('vnClientDescriptor', { diff --git a/modules/client/front/index.js b/modules/client/front/index.js index 324046206..7df6f7b10 100644 --- a/modules/client/front/index.js +++ b/modules/client/front/index.js @@ -40,3 +40,5 @@ import './postcode'; import './dms/index'; import './dms/create'; import './dms/edit'; +import './consumption'; +import './consumption-search-panel'; diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index a20acf6cb..e332a0229 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -56,4 +56,5 @@ Requested credits: Créditos solicitados Contacts: Contactos Samples: Plantillas Send sample: Enviar plantilla -Log: Historial \ No newline at end of file +Log: Historial +Consumption: Consumo \ No newline at end of file diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json index a7920f403..2a5f63bb2 100644 --- a/modules/client/front/routes.json +++ b/modules/client/front/routes.json @@ -18,16 +18,17 @@ {"state": "client.card.greuge.index", "icon": "work"}, {"state": "client.card.balance.index", "icon": "icon-invoices"}, {"state": "client.card.recovery.index", "icon": "icon-recovery"}, + {"state": "client.card.webAccess", "icon": "cloud"}, {"state": "client.card.log", "icon": "history"}, { "description": "Others", "icon": "more", "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.creditInsurance.index", "icon": "icon-solunion"}, {"state": "client.card.contact", "icon": "contact_phone"}, - {"state": "client.card.sample.index", "icon": "mail"}, {"state": "client.card.webPayment", "icon": "icon-onlinepayment"}, {"state": "client.card.dms.index", "icon": "cloud_upload"} ] @@ -350,6 +351,15 @@ "params": { "client": "$ctrl.client" } + }, + { + "url": "/consumption", + "state": "client.card.consumption", + "component": "vn-client-consumption", + "description": "Consumption", + "params": { + "client": "$ctrl.client" + } } ] } diff --git a/print/core/components/email-footer/assets/css/style.css b/print/core/components/email-footer/assets/css/style.css index 4bc22fdfd..29620a64e 100644 --- a/print/core/components/email-footer/assets/css/style.css +++ b/print/core/components/email-footer/assets/css/style.css @@ -46,6 +46,7 @@ } .privacy { + text-align: center; padding: 20px 0; font-size: 10px; font-weight: 100 diff --git a/print/core/components/email-header/assets/css/style.css b/print/core/components/email-header/assets/css/style.css index 4db5e2b2e..e6451ca5a 100644 --- a/print/core/components/email-header/assets/css/style.css +++ b/print/core/components/email-header/assets/css/style.css @@ -1,5 +1,10 @@ +header { + text-align: center +} + header .logo { - margin-bottom: 15px; + margin-top: 25px; + margin-bottom: 25px } header .logo img { diff --git a/print/core/filters/date.js b/print/core/filters/date.js index 0988eda75..5d1bc0de5 100644 --- a/print/core/filters/date.js +++ b/print/core/filters/date.js @@ -2,5 +2,6 @@ const Vue = require('vue'); const strftime = require('strftime'); Vue.filter('date', function(value, specifiers = '%d-%m-%Y') { + if (!(value instanceof Date)) value = new Date(value); return strftime(specifiers, value); }); diff --git a/print/templates/email/campaign-metrics/attachments.json b/print/templates/email/campaign-metrics/attachments.json index 3f6a93bb5..d836d6040 100644 --- a/print/templates/email/campaign-metrics/attachments.json +++ b/print/templates/email/campaign-metrics/attachments.json @@ -1,6 +1,6 @@ [ { - "filename": "campaing-metrics", + "filename": "campaign-metrics.pdf", "component": "campaign-metrics" } ] \ No newline at end of file diff --git a/print/templates/email/campaign-metrics/campaign-metrics.html b/print/templates/email/campaign-metrics/campaign-metrics.html index 4ba95adb9..9d7014f34 100644 --- a/print/templates/email/campaign-metrics/campaign-metrics.html +++ b/print/templates/email/campaign-metrics/campaign-metrics.html @@ -25,7 +25,7 @@

{{ $t('title') }}

{{$t('dear')}},

-

{{$t('description')}}

+

diff --git a/print/templates/email/campaign-metrics/campaign-metrics.js b/print/templates/email/campaign-metrics/campaign-metrics.js index 51d2ebb44..0ace0fc25 100755 --- a/print/templates/email/campaign-metrics/campaign-metrics.js +++ b/print/templates/email/campaign-metrics/campaign-metrics.js @@ -4,7 +4,17 @@ const emailFooter = new Component('email-footer'); module.exports = { 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: { 'email-header': emailHeader.build(), 'email-footer': emailFooter.build() diff --git a/print/templates/email/campaign-metrics/locale/es.yml b/print/templates/email/campaign-metrics/locale/es.yml index e662ca614..d1c1182a2 100644 --- a/print/templates/email/campaign-metrics/locale/es.yml +++ b/print/templates/email/campaign-metrics/locale/es.yml @@ -1,7 +1,8 @@ -subject: Informe consumo campaña -title: Informe consumo campaña +subject: Informe de consumo +title: Informe de consumo 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 - mismas fechas del año pasado. Espero le sea de utilidad para preparar su pedido. + fechas comprendidas entre {0} y {1}. + Espero le sea de utilidad para preparar su pedido.

Al mismo tiempo aprovecho la ocasión para saludarle cordialmente. diff --git a/print/templates/reports/campaign-metrics/campaign-metrics.js b/print/templates/reports/campaign-metrics/campaign-metrics.js index ef1d735de..420c1ffb1 100755 --- a/print/templates/reports/campaign-metrics/campaign-metrics.js +++ b/print/templates/reports/campaign-metrics/campaign-metrics.js @@ -6,9 +6,6 @@ const reportFooter = new Component('report-footer'); module.exports = { name: 'campaign-metrics', async serverPrefetch() { - this.to = new Date(this.to); - this.from = new Date(this.from); - this.client = await this.fetchClient(this.recipientId); this.sales = await this.fetchSales(this.recipientId, this.from, this.to); @@ -54,7 +51,7 @@ module.exports = { t.clientFk = ? AND it.isPackaging = FALSE AND DATE(t.shipped) BETWEEN ? AND ? 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: { @@ -66,12 +63,10 @@ module.exports = { required: true }, from: { - required: true, - type: Date + required: true }, to: { - required: true, - type: Date + required: true } } }; diff --git a/print/templates/reports/campaign-metrics/locale/es.yml b/print/templates/reports/campaign-metrics/locale/es.yml index df0c91971..8a4cc4637 100644 --- a/print/templates/reports/campaign-metrics/locale/es.yml +++ b/print/templates/reports/campaign-metrics/locale/es.yml @@ -1,4 +1,4 @@ -title: Consumo de campaña +title: Consumo Client: Cliente clientData: Datos del cliente dated: Fecha