feat(client): detail section with smart-table
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Joan Sanchez 2022-05-31 07:56:17 +02:00
parent f66e5fac9e
commit a5e833af2c
10 changed files with 726 additions and 0 deletions

View File

@ -0,0 +1,3 @@
INSERT INTO salix.defaultViewConfig (tableCode, columns)
VALUES ('clientsDetail', '{"id":true,"phone":true,"city":true,"socialName":true,"salesPersonFk":true,"email":true}');

View File

@ -0,0 +1,202 @@
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('detailFilter', {
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: 'tags',
type: ['object'],
description: 'List of tags to filter with',
},
{
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by name`,
},
{
arg: 'id',
type: 'integer',
description: 'Item id',
},
{
arg: 'categoryFk',
type: 'integer',
description: 'Category id',
},
{
arg: 'typeFk',
type: 'integer',
description: 'Type id',
},
{
arg: 'isActive',
type: 'boolean',
description: 'Whether the item is or not active',
},
{
arg: 'buyerFk',
type: 'integer',
description: 'The buyer of the item',
},
{
arg: 'supplierFk',
type: 'integer',
description: 'The supplier of the item',
},
{
arg: 'description',
type: 'string',
description: 'The item description',
},
{
arg: 'stemMultiplier',
type: 'integer',
description: 'The item multiplier',
},
{
arg: 'landed',
type: 'date',
description: 'The item last buy landed date',
},
{
arg: 'isFloramondo',
type: 'boolean',
description: 'Whether the the item is or not floramondo',
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/detailFilter`,
verb: 'GET'
}
});
Self.detailFilter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
let codeWhere;
if (ctx.args.search) {
const items = await Self.app.models.ItemBarcode.find({
where: {code: ctx.args.search},
fields: ['itemFk']
}, myOptions);
const itemIds = [];
for (const item of items)
itemIds.push(item.itemFk);
codeWhere = {'i.id': {inq: itemIds}};
}
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {or: [{'i.id': value}, codeWhere]}
: {or: [
{'i.name': {like: `%${value}%`}},
{'i.longName': {like: `%${value}%`}},
codeWhere]};
case 'id':
case 'isActive':
case 'typeFk':
case 'isFloramondo':
return {[`i.${param}`]: value};
case 'multiplier':
return {'i.stemMultiplier': value};
case 'categoryFk':
return {'ic.id': value};
case 'buyerFk':
return {'it.workerFk': value};
case 'supplierFk':
return {'s.id': value};
case 'origin':
return {'ori.code': value};
case 'intrastat':
return {'intr.description': value};
case 'landed':
return {'lb.landed': value};
}
});
filter = mergeFilters(filter, {where});
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT
c.id,
c.name,
c.socialName,
c.fi,
c.credit,
c.creditInsurance,
c.phone,
c.mobile,
c.street,
c.city,
c.postcode,
c.email,
c.created,
c.isActive,
c.isVies,
c.isTaxDataChecked,
c.isEqualizated,
c.isFreezed,
c.hasToInvoice,
c.hasToInvoiceByAddress,
c.isToBeMailed,
c.hasSepaVnl,
c.hasLcr,
c.hasCoreVnl,
ct.id AS countryFk,
ct.country,
p.id AS provinceFk,
p.name AS province,
u.id AS salesPersonFk,
u.name AS salesPerson,
bt.code AS businessTypeFk,
bt.description AS businessType,
sti.CodigoIva AS sageTaxTypeFk,
sti.Iva AS sageTaxType,
stt.CodigoTransaccion AS sageTransactionTypeFk,
stt.Transaccion AS sageTransactionType
FROM client c
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN country ct ON ct.id = c.countryFk
LEFT JOIN province p ON p.id = c.provinceFk
LEFT JOIN businessType bt ON bt.code = c.businessTypeFk
LEFT JOIN sage.TiposIva sti ON sti.CodigoIva = c.taxTypeSageFk
LEFT JOIN sage.TiposTransacciones stt ON stt.CodigoTransaccion = c.transactionTypeSageFk
`
);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makePagination(filter));
const clientsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return clientsIndex === 0 ? result : result[clientsIndex];
};
};

View File

@ -31,6 +31,7 @@ module.exports = Self => {
require('../methods/client/createReceipt')(Self);
require('../methods/client/updatePortfolio')(Self);
require('../methods/client/checkDuplicated')(Self);
require('../methods/client/detailFilter')(Self);
// Validations

View File

@ -0,0 +1,335 @@
<vn-crud-model
vn-id="model"
url="Clients/detailFilter"
limit="20"
auto-load="true">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
placeholder="Search client"
info="Search client by id or name"
auto-state="false"
model="model">
</vn-searchbar>
</vn-portal>
<vn-card>
<smart-table
model="model"
view-config-id="clientsDetail"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-table>
<table>
<thead>
<tr>
<th></th>
<th field="id">
<span translate>Identifier</span>
</th>
<th field="name">
<span translate>Name</span>
</th>
<th field="socialName">
<span translate>Social name</span>
</th>
<th field="fi">
<span translate>Fiscal ID</span>
</th>
<th field="salesPersonFk">
<span translate>Salesperson</span>
</th>
<th field="credit">
<span translate>Credit</span>
</th>
<th field="creditInsurance">
<span translate>Credit insurance</span>
</th>
<th field="phone">
<span translate>Phone</span>
</th>
<th field="mobile">
<span translate>Mobile</span>
</th>
<th field="street">
<span translate>Street</span>
</th>
<th field="countryFk">
<span translate>Country</span>
</th>
<th field="provinceFk">
<span translate>Province</span>
</th>
<th field="city">
<span translate>City</span>
</th>
<th field="postcode">
<span translate>Postcode</span>
</th>
<th field="email">
<span translate>Email</span>
</th>
<th field="created">
<span translate>Created</span>
</th>
<th field="businessTypeFk">
<span translate>Business type</span>
</th>
<th field="sageTransactionTypeFk">
<span translate>Sage tax type</span>
</th>
<th field="sageTransactionTypeFk">
<span translate>Sage tr. type</span>
</th>
<th field="isActive">
<span translate>Active</span>
</th>
<th field="isVies">
<span translate>Vies</span>
</th>
<th field="isTaxDataChecked">
<span translate>Tax data checked</span>
</th>
<th field="isEqualizated">
<span translate>Tax equalized</span>
</th>
<th field="isFreezed">
<span translate>Freezed</span>
</th>
<th field="hasToInvoice">
<span translate>Invoice</span>
</th>
<th field="hasToInvoiceByAddress">
<span translate>Invoice by address</span>
</th>
<th field="isToBeMailed">
<span translate>Mailing</span>
</th>
<th field="hasLcr">
<span translate>Received LCR</span>
</th>
<th field="hasCoreVnl">
<span translate>Received core VNL</span>
</th>
<th field="hasSepaVnl">
<span translate>Received B2B VNL</span>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in model.data"
vn-anchor="::{
state: 'client.card.summary',
params: {id: client.id}
}">
<td>
<vn-icon-button ng-show="client.isActive == false"
vn-tooltip="Client inactive"
icon="icon-disabled">
</vn-icon-button>
<vn-icon-button ng-show="client.isActive && client.isFreezed == true"
vn-tooltip="Client frozen"
icon="icon-frozen">
</vn-icon-button>
</td>
<td>
<span
vn-click-stop="clientDescriptor.show($event, client.id)"
class="link">
{{::client.id}}
</span>
</td>
<td>{{::client.name}}</td>
<td>{{::client.socialName}}</td>
<td>{{::client.fi}}</td>
<td>
<span
vn-click-stop="workerDescriptor.show($event, client.salesPersonFk)"
ng-class="{'link': client.salesPersonFk}">
{{::client.salesPerson | dashIfEmpty}}
</span>
</td>
<td>{{::client.credit}}</td>
<td>{{::client.creditInsurance | dashIfEmpty}}</td>
<td>{{::client.phone | dashIfEmpty}}</td>
<td>{{::client.mobile | dashIfEmpty}}</td>
<td>{{::client.street | dashIfEmpty}}</td>
<td>{{::client.country | dashIfEmpty}}</td>
<td>{{::client.province | dashIfEmpty}}</td>
<td>{{::client.city | dashIfEmpty}}</td>
<td>{{::client.postcode | dashIfEmpty}}</td>
<td>{{::client.email | dashIfEmpty}}</td>
<td>{{::client.created | date:'dd/MM/yyyy'}}</td>
<td>{{::client.businessType | dashIfEmpty}}</td>
<td>{{::client.sageTaxType | dashIfEmpty}}</td>
<td>{{::client.sageTransactionType | dashIfEmpty}}</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isActive">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isVies">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isTaxDataChecked">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isEqualizated">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isFreezed">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.hasToInvoice">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.hasToInvoiceByAddress">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.isToBeMailed">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.hasLcr">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.hasCoreVnl">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="::client.hasSepaVnl">
</vn-check>
</td>
<!-- <td vn-fetched-tags>
<div>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</td>
<td>{{::item.stems}}</td>
<td>{{::item.size}}</td>
<td title="{{::item.typeName}}">
{{::item.typeName}}
</td>
<td title="{{::item.category}}">
{{::item.category}}
</td>
<td title="{{::item.intrastat}}">
{{::item.intrastat}}
</td>
<td>{{::item.origin}}</td>
<td title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</td>
<td>{{::item.density}}</td>
<td>{{::item.stemMultiplier}}</td>
<td>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</td>
<td>{{::item.producer | dashIfEmpty}}</td>
<td shrink-date>{{::item.landed | date:'dd/MM/yyyy'}}</td> -->
<td shrink>
<vn-horizontal class="buttons">
<vn-icon-button vn-anchor="{state: 'ticket.index', params: {q: {clientFk: client.id} } }"
vn-tooltip="Client tickets"
icon="icon-ticket">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(client)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="preview">
<vn-client-summary
client="$ctrl.clientSelected">
</vn-client-summary>
</vn-popup>
<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>
</slot-menu>
</vn-contextmenu>

View File

@ -0,0 +1,113 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.smartTableOptions = {
activeButtons: {
search: true,
shownColumns: true,
},
columns: [
{
field: 'category',
autocomplete: {
url: 'ItemCategories',
valueField: 'name',
}
},
{
field: 'origin',
autocomplete: {
url: 'Origins',
showField: 'code',
valueField: 'code'
}
},
{
field: 'typeFk',
autocomplete: {
url: 'ItemTypes',
}
},
{
field: 'intrastat',
autocomplete: {
url: 'Intrastats',
showField: 'description',
valueField: 'description'
}
},
{
field: 'buyerFk',
autocomplete: {
url: 'Workers/activeWithRole',
where: `{role: {inq: ['logistic', 'buyer']}}`,
searchFunction: '{firstName: $search}',
showField: 'nickname',
valueField: 'id',
}
},
{
field: 'active',
searchable: false
},
{
field: 'landed',
searchable: false
},
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'category':
return {'ic.name': value};
case 'buyerFk':
return {'it.workerFk': value};
case 'grouping':
return {'b.grouping': value};
case 'packing':
return {'b.packing': value};
case 'origin':
return {'ori.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'intrastat':
return {'intr.description': value};
case 'name':
return {'i.name': {like: `%${value}%`}};
case 'producer':
return {'pr.name': {like: `%${value}%`}};
case 'id':
case 'size':
case 'subname':
case 'isActive':
case 'density':
case 'stemMultiplier':
case 'stems':
return {[`i.${param}`]: value};
}
}
onCloneAccept(itemFk) {
return this.$http.post(`Items/${itemFk}/clone`)
.then(res => {
this.$state.go('item.card.tags', {id: res.data.id});
});
}
preview(client) {
this.clientSelected = client;
this.$.preview.show();
}
}
ngModule.vnComponent('vnClientDetail', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,30 @@
import './index.js';
describe('Item', () => {
describe('Component vnItemIndex', () => {
let controller;
let $httpBackend;
let $scope;
beforeEach(ngModule('item'));
beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
const $element = angular.element('<vn-item-index></vn-item-index>');
controller = $componentController('vnItemIndex', {$element, $scope});
}));
describe('onCloneAccept()', () => {
it('should perform a post query and then call go() then update itemSelected in the controller', () => {
jest.spyOn(controller.$state, 'go');
$httpBackend.expectRoute('POST', `Items/:id/clone`).respond({id: 99});
controller.onCloneAccept(1);
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('item.card.tags', {id: 99});
});
});
});
});

View File

@ -0,0 +1,2 @@
picture: Foto
Buy requests: Peticiones de compra

View File

@ -0,0 +1,32 @@
@import "variables";
vn-item-product {
display: block;
.id {
background-color: $color-main;
color: $color-font-dark;
margin-bottom: 0;
}
.image {
height: 112px;
width: 112px;
& > img {
max-height: 100%;
max-width: 100%;
border-radius: 3px;
}
}
vn-label-value:first-of-type section{
margin-top: 9px;
}
}
table {
img {
border-radius: 50%;
width: 50px;
height: 50px;
}
}

View File

@ -47,3 +47,4 @@ import './consumption-search-panel';
import './defaulter';
import './notification';
import './unpaid';
import './detail';

View File

@ -7,6 +7,7 @@
"menus": {
"main": [
{"state": "client.index", "icon": "person"},
{"state": "client.detail", "icon": "person"},
{"state": "client.notification", "icon": "campaign"},
{"state": "client.defaulter", "icon": "icon-defaulter"}
],
@ -381,6 +382,12 @@
"component": "vn-client-unpaid",
"acl": ["administrative"],
"description": "Unpaid"
},
{
"url": "/detail",
"state": "client.detail",
"component": "vn-client-detail",
"description": "Detail"
}
]
}