3315 - feat(notification): notify client metrics #876

Merged
carlosjr merged 3 commits from 3315-notify_metrics into dev 2022-02-15 11:19:43 +00:00
9 changed files with 451 additions and 0 deletions

View File

@ -45,3 +45,4 @@ import './dms/edit';
import './consumption'; import './consumption';
import './consumption-search-panel'; import './consumption-search-panel';
import './defaulter'; import './defaulter';
import './notification';

View File

@ -0,0 +1,155 @@
<vn-crud-model
vn-id="model"
url="Clients"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-client-search-panel"
placeholder="Search client"
info="Search client by id or name"
auto-state="false"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
</vn-searchbar>
</vn-portal>
<vn-card>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<vn-button disabled="$ctrl.checked.length === 0"
icon="show_chart"
ng-click="filters.show($event)"
vn-tooltip="Campaign consumption">
</vn-button>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th shrink>
<vn-multi-check
model="model"
check-field="$checked">
</vn-multi-check>
</th>
<th field="id">
<span translate>Identifier</span>
</th>
<th field="socialName">
<span translate>Social name</span>
</th>
<th field="city">
<span translate>City</span>
</th>
<th field="phone">
<span translate>Phone</span>
</th>
<th field="email">
<span translate>Email</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in model.data"
vn-anchor="::{
state: 'item.card.summary',
params: {id: item.id}
}">
<td>
<vn-check
ng-model="client.$checked"
vn-click-stop>
</vn-check>
</td>
<td>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::client.id}}
</span>
</td>
<td>{{::client.socialName}}</td>
<td>{{::client.city}}</td>
<td>{{::client.phone}}</td>
<td>{{::client.email}}</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-popover vn-id="filters">
<div class="vn-pa-lg">
<form ng-submit="$ctrl.onSendClientConsumption()">
<vn-horizontal><h4 translate>Campaign consumption</h4></vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
url="Campaigns/latest"
label="Campaign"
translate-fields="['code']"
show-field="code"
value-field="id"
ng-model="$ctrl.campaign.id"
order="dated DESC"
selection="$ctrl.campaignSelection"
search-function="{dated: {like: '%'+ $search +'%'}}">
<tpl-item>
{{code}} {{dated | date: 'yyyy'}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From"
ng-model="$ctrl.campaign.from"
on-change="$ctrl.onChangeDate(value)">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="$ctrl.campaign.to"
on-change="$ctrl.onChangeDate(value)">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Send"></vn-submit>
</vn-horizontal>
</form>
</div>
</vn-popover>
<vn-contextmenu vn-id="contextmenu" targets="['smart-table']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -0,0 +1,119 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.$checkAll = false;
this.smartTableOptions = {
activeButtons: {
search: true
},
columns: [
{
field: 'socialName',
autocomplete: {
url: 'Clients',
showField: 'socialName',
valueField: 'socialName'
}
},
{
field: 'city',
autocomplete: {
url: 'Towns',
valueField: 'name',
showField: 'name'
}
}
]
};
this.campaign = {
id: null,
from: null,
to: null
};
this.getUpcomingCampaing();
}
get checked() {
const clients = this.$.model.data || [];
const checkedClients = [];
for (const buy of clients) {
if (buy.$checked)
checkedClients.push(buy);
}
return checkedClients;
}
onChangeDate(value) {
if (value)
this.campaign.id = null;
}
getUpcomingCampaing() {
this.$http.get('Campaigns/upcoming')
.then(res => this.campaign.id = res.data.id);
}
get campaignSelection() {
return this._campaignSelection;
}
set campaignSelection(value) {
this._campaignSelection = value;
if (value) {
const from = new Date(value.dated);
from.setDate(from.getDate() - value.scopeDays);
this.campaign.to = value.dated;
this.campaign.from = from;
}
}
onSendClientConsumption() {
const clientIds = this.checked.map(client => client.id);
const params = Object.assign({
clientIds: clientIds
}, this.campaign);
this.$http.post('notify/consumption', params)
.then(() => this.$.filters.hide())
.then(() => this.vnApp.showSuccess(this.$t('Notifications sent!')));
}
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {id: value}
: {or: [{name: {like: `%${value}%`}}, {socialName: {like: `%${value}%`}}]};
case 'phone':
return {
or: [
{phone: value},
{mobile: value}
]
};
case 'name':
case 'socialName':
case 'city':
case 'email':
return {[param]: {like: `%${value}%`}};
case 'id':
case 'fi':
case 'postcode':
case 'salesPersonFk':
return {[param]: value};
}
}
}
ngModule.vnComponent('vnClientNotification', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,99 @@
import './index';
import crudModel from 'core/mocks/crud-model';
describe('Client notification', () => {
describe('Component vnClientNotification', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('client'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-client-notification></vn-client-notification>');
controller = $componentController('vnClientNotification', {$element});
controller.$.model = crudModel;
controller.$.model.data = [
{id: 1101},
{id: 1102},
{id: 1103}
];
$httpBackend.expect('GET', `Campaigns/upcoming`).respond(200, {id: 1});
}));
describe('checked() getter', () => {
it('should return the checked lines', () => {
const data = controller.$.model.data;
data[1].$checked = true;
data[2].$checked = true;
const checkedRows = controller.checked;
const firstCheckedRow = checkedRows[0];
const secondCheckedRow = checkedRows[1];
expect(firstCheckedRow.id).toEqual(1102);
expect(secondCheckedRow.id).toEqual(1103);
});
});
describe('campaignSelection() setter', () => {
it('should set the campaign from and to properties', () => {
const dated = new Date();
controller.campaignSelection = {
dated: dated,
scopeDays: 14
};
const expectedDateTo = new Date(dated);
expectedDateTo.setDate(expectedDateTo.getDate() - 14);
const campaign = controller.campaign;
expect(campaign.from).toEqual(expectedDateTo);
expect(campaign.to).toEqual(dated);
});
});
describe('onSendClientConsumption()', () => {
it('should return saved message', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$.filters = {hide: () => {}};
controller.campaign = {
id: 1,
from: new Date(),
to: new Date()
};
const data = controller.$.model.data;
data[0].$checked = true;
data[1].$checked = true;
const params = Object.assign({
clientIds: [1101, 1102]
}, controller.campaign);
$httpBackend.expect('POST', `notify/consumption`, params).respond(200, params);
controller.onSendClientConsumption();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Notifications sent!');
});
});
describe('exprBuilder()', () => {
it('should search by sales person', () => {
let expr = controller.exprBuilder('salesPersonFk', '5');
expect(expr).toEqual({'salesPersonFk': '5'});
});
it('should search by client social name', () => {
let expr = controller.exprBuilder('socialName', '1foo');
expect(expr).toEqual({'socialName': {like: '%1foo%'}});
});
});
});
});

View File

@ -0,0 +1,2 @@
Campaign consumption: Consumo campaña
Send: Enviar

View File

@ -7,6 +7,7 @@
"menus": { "menus": {
"main": [ "main": [
{"state": "client.index", "icon": "person"}, {"state": "client.index", "icon": "person"},
{"state": "client.notification", "icon": "campaign"},
{"state": "client.defaulter.index", "icon": "icon-defaulter"} {"state": "client.defaulter.index", "icon": "icon-defaulter"}
], ],
"card": [ "card": [
@ -373,6 +374,12 @@
"state": "client.defaulter.index", "state": "client.defaulter.index",
"component": "vn-client-defaulter-index", "component": "vn-client-defaulter-index",
"description": "Defaulter" "description": "Defaulter"
},
{
"url" : "/notification",
"state": "client.notification",
"component": "vn-client-notification",
"description": "Notifications"
} }
] ]
} }

View File

@ -0,0 +1,58 @@
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.body;
if (!reqArgs.clientIds)
throw new Error('The argument clientIds is required');
if (!reqArgs.from)
throw new Error('The argument from is required');
if (!reqArgs.to)
throw new Error('The argument to is required');
response.status(200).json({
message: 'Success'
});
const clientIds = reqArgs.clientIds;
const clients = await db.rawSql(`
SELECT
c.id,
c.email,
eu.email salesPersonEmail
FROM client c
JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
JOIN ticket t ON t.clientFk = c.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE c.id IN(?)
AND it.isPackaging = FALSE
AND DATE(t.shipped) BETWEEN ? AND ?
GROUP BY c.id`, [clientIds, reqArgs.from, reqArgs.to]);
const clientData = new Map();
for (const client of clients)
clientData.set(client.id, client);
for (const clientId of reqArgs.clientIds) {
const client = clientData.get(clientId);
if (client) {
const args = Object.assign({
recipientId: clientId,
recipient: client.email,
replyTo: client.salesPersonEmail
}, response.locals);
const email = new Email('campaign-metrics', args);
await email.send();
}
}
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,6 @@
const express = require('express');
const router = new express.Router();
router.post('/consumption', require('./consumption'));
module.exports = router;

View File

@ -15,4 +15,8 @@ module.exports = [
url: '/api/closure', url: '/api/closure',
cb: require('./closure') cb: require('./closure')
}, },
{
url: '/api/notify',
cb: require('./notify')
}
]; ];