diff --git a/db/changes/10470-family/00-defaultViewConfig.sql b/db/changes/10470-family/00-defaultViewConfig.sql new file mode 100644 index 000000000..d423599b1 --- /dev/null +++ b/db/changes/10470-family/00-defaultViewConfig.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns) +VALUES ('clientsDetail', '{"id":true,"phone":true,"city":true,"socialName":true,"salesPersonFk":true,"email":true,"name":false,"fi":false,"credit":false,"creditInsurance":false,"mobile":false,"street":false,"countryFk":false,"provinceFk":false,"postcode":false,"created":false,"businessTypeFk":false,"payMethodFk":false,"sageTaxTypeFk":false,"sageTransactionTypeFk":false,"isActive":false,"isVies":false,"isTaxDataChecked":false,"isEqualizated":false,"isFreezed":false,"hasToInvoice":false,"hasToInvoiceByAddress":false,"isToBeMailed":false,"hasLcr":false,"hasCoreVnl":false,"hasSepaVnl":false}'); + diff --git a/front/core/components/smart-table/index.js b/front/core/components/smart-table/index.js index 81d8d103e..401541c2c 100644 --- a/front/core/components/smart-table/index.js +++ b/front/core/components/smart-table/index.js @@ -318,6 +318,8 @@ export default class SmartTable extends Component { for (let column of columns) { const field = column.getAttribute('field'); const cell = document.createElement('td'); + cell.setAttribute('centered', ''); + if (field) { let input; let options; @@ -331,6 +333,15 @@ export default class SmartTable extends Component { continue; } + input = this.$compile(` + `)(this.$inputsScope); + if (options && options.autocomplete) { let props = ``; @@ -346,16 +357,29 @@ export default class SmartTable extends Component { on-change="$ctrl.searchByColumn('${field}')" clear-disabled="true" />`)(this.$inputsScope); - } else { + } + + if (options && options.checkbox) { input = this.$compile(` - `)(this.$inputsScope); } + + if (options && options.datepicker) { + input = this.$compile(` + `)(this.$inputsScope); + } + cell.appendChild(input[0]); } searchRow.appendChild(cell); @@ -372,13 +396,12 @@ export default class SmartTable extends Component { searchByColumn(field) { const searchCriteria = this.$inputsScope.searchProps[field]; - const emptySearch = searchCriteria == '' || null; + const emptySearch = searchCriteria === '' || searchCriteria == null; const filters = this.filterSanitizer(field); if (filters && filters.userFilter) this.model.userFilter = filters.userFilter; - if (!emptySearch) this.addFilter(field, this.$inputsScope.searchProps[field]); else this.model.refresh(); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index a44ba2da8..9e2b8989b 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -224,5 +224,7 @@ "The agency is already assigned to another autonomous": "La agencia ya está asignada a otro autónomo", "date in the future": "Fecha en el futuro", "reference duplicated": "Referencia duplicada", - "This ticket is already a refund": "Este ticket ya es un abono" + "This ticket is already a refund": "Este ticket ya es un abono", + "isWithoutNegatives": "isWithoutNegatives", + "routeFk": "routeFk" } \ No newline at end of file diff --git a/modules/client/back/methods/client/extendedListFilter.js b/modules/client/back/methods/client/extendedListFilter.js new file mode 100644 index 000000000..8e02cd413 --- /dev/null +++ b/modules/client/back/methods/client/extendedListFilter.js @@ -0,0 +1,159 @@ + +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('extendedListFilter', { + description: 'Find all clients matched by the filter', + accessType: 'READ', + accepts: [ + { + arg: 'filter', + type: 'object', + }, + { + arg: 'search', + type: 'string', + description: `If it's and integer searchs by id, otherwise it searchs by name`, + }, + { + arg: 'name', + type: 'string', + description: 'The client name', + }, + { + arg: 'salesPersonFk', + type: 'number', + }, + { + arg: 'fi', + type: 'string', + description: 'The client fiscal id', + }, + { + arg: 'socialName', + type: 'string', + }, + { + arg: 'city', + type: 'string', + }, + { + arg: 'postcode', + type: 'string', + }, + { + arg: 'provinceFk', + type: 'number', + }, + { + arg: 'email', + type: 'string', + }, + { + arg: 'phone', + type: 'string', + }, + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/extendedListFilter`, + verb: 'GET' + } + }); + + Self.extendedListFilter = async(ctx, filter, options) => { + const conn = Self.dataSource.connector; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const where = buildFilter(ctx.args, (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {'c.id': {inq: value}} + : {'c.name': {like: `%${value}%`}}; + case 'name': + case 'salesPersonFk': + case 'fi': + case 'socialName': + case 'city': + case 'postcode': + case 'provinceFk': + case 'email': + case 'phone': + param = `c.${param}`; + return {[param]: 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, + pm.id AS payMethodFk, + pm.name AS payMethod, + 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 payMethod pm ON pm.id = c.payMethodFk + 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]; + }; +}; diff --git a/modules/client/back/methods/client/specs/extendedListFilter.spec.js b/modules/client/back/methods/client/specs/extendedListFilter.spec.js new file mode 100644 index 000000000..907c03ef9 --- /dev/null +++ b/modules/client/back/methods/client/specs/extendedListFilter.spec.js @@ -0,0 +1,180 @@ +const { models } = require('vn-loopback/server/server'); + +describe('client extendedListFilter()', () => { + it('should return the clients matching the filter with a limit of 20 rows', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {}}; + const filter = {limit: '20'}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + expect(result.length).toEqual(20); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his name', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the search argument with his id', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {search: '1101'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the client "Bruce Wayne" matching the name argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {name: 'Bruce Wayne'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstResult = result[0]; + + expect(result.length).toEqual(1); + expect(firstResult.name).toEqual('Bruce Wayne'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "salesPersonFk" argument', async() => { + const tx = await models.Client.beginTransaction({}); + const salesPersonId = 18; + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {salesPersonFk: salesPersonId}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(5); + expect(randomResultClient.salesPersonFk).toEqual(salesPersonId); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "fi" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {fi: '251628698'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const firstClient = result[0]; + + expect(result.length).toEqual(1); + expect(firstClient.name).toEqual('Max Eisenhardt'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "city" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {city: 'Silla'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.city.toLowerCase()).toEqual('silla'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the clients matching the "postcode" argument', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}, args: {postcode: '46460'}}; + const filter = {}; + const result = await models.Client.extendedListFilter(ctx, filter, options); + + const randomIndex = Math.floor(Math.random() * result.length); + const randomResultClient = result[randomIndex]; + + expect(result.length).toBeGreaterThanOrEqual(20); + expect(randomResultClient.postcode).toEqual('46460'); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index fb41fb973..90a9b9e23 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -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/extendedListFilter')(Self); // Validations diff --git a/modules/client/front/extended-list/index.html b/modules/client/front/extended-list/index.html new file mode 100644 index 000000000..b45a0bc5f --- /dev/null +++ b/modules/client/front/extended-list/index.html @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Identifier + + Name + + Social name + + Tax number + + Salesperson + + Credit + + Credit insurance + + Phone + + Mobile + + Street + + Country + + Province + + City + + Postcode + + Email + + Created + + Business type + + Billing data + + Sage tax type + + Sage tr. type + + Active + + Vies + + Verified data + + Is equalizated + + Freezed + + Invoice + + Invoice by address + + Mailing + + Received LCR + + Received core VNL + + Received B2B VNL +
+ + + + + + + {{::client.id}} + + {{::client.name}}{{::client.socialName}}{{::client.fi}} + + {{::client.salesPerson | dashIfEmpty}} + + {{::client.credit}}{{::client.creditInsurance | dashIfEmpty}}{{::client.phone | dashIfEmpty}}{{::client.mobile | dashIfEmpty}}{{::client.street | dashIfEmpty}}{{::client.country | dashIfEmpty}}{{::client.province | dashIfEmpty}}{{::client.city | dashIfEmpty}}{{::client.postcode | dashIfEmpty}}{{::client.email | dashIfEmpty}}{{::client.created | date:'dd/MM/yyyy'}}{{::client.businessType | dashIfEmpty}}{{::client.payMethod | dashIfEmpty}}{{::client.sageTaxType | dashIfEmpty}}{{::client.sageTransactionType | dashIfEmpty}} + + {{ ::client.isActive ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isVies ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isTaxDataChecked ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isEqualizated ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isFreezed ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasToInvoice ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasToInvoiceByAddress ? 'Yes' : 'No' | translate}} + + + + {{ ::client.isToBeMailed ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasLcr ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasCoreVnl ? 'Yes' : 'No' | translate}} + + + + {{ ::client.hasSepaVnl ? 'Yes' : 'No' | translate}} + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + \ No newline at end of file diff --git a/modules/client/front/extended-list/index.js b/modules/client/front/extended-list/index.js new file mode 100644 index 000000000..8eed48d01 --- /dev/null +++ b/modules/client/front/extended-list/index.js @@ -0,0 +1,184 @@ +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: 'socialName', + autocomplete: { + url: 'Clients', + showField: 'socialName', + valueField: 'socialName', + } + }, + { + field: 'created', + datepicker: true + }, + { + field: 'countryFk', + autocomplete: { + url: 'Countries', + showField: 'country', + } + }, + { + field: 'provinceFk', + autocomplete: { + url: 'Provinces' + } + }, + { + field: 'salesPersonFk', + autocomplete: { + url: 'Workers/activeWithInheritedRole', + where: `{role: 'salesPerson'}`, + searchFunction: '{firstName: $search}', + showField: 'nickname', + valueField: 'id', + } + }, + { + field: 'businessTypeFk', + autocomplete: { + url: 'BusinessTypes', + valueField: 'code', + showField: 'description', + } + }, + { + field: 'payMethodFk', + autocomplete: { + url: 'PayMethods', + } + }, + { + field: 'sageTaxTypeFk', + autocomplete: { + url: 'SageTaxTypes', + showField: 'vat', + } + }, + { + field: 'sageTransactionTypeFk', + autocomplete: { + url: 'SageTransactionTypes', + showField: 'transaction', + } + }, + { + field: 'isActive', + checkbox: true + }, + { + field: 'isVies', + checkbox: true + }, + { + field: 'isTaxDataChecked', + checkbox: true + }, + { + field: 'isEqualizated', + checkbox: true + }, + { + field: 'isFreezed', + checkbox: true + }, + { + field: 'hasToInvoice', + checkbox: true + }, + { + field: 'hasToInvoiceByAddress', + checkbox: true + }, + { + field: 'isToBeMailed', + checkbox: true + }, + { + field: 'hasSepaVnl', + checkbox: true + }, + { + field: 'hasLcr', + checkbox: true + }, + { + field: 'hasCoreVnl', + checkbox: true + } + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'created': + return {'c.created': { + between: this.dateRange(value)} + }; + case 'id': + case 'name': + case 'socialName': + case 'fi': + case 'credit': + case 'creditInsurance': + case 'phone': + case 'mobile': + case 'street': + case 'city': + case 'postcode': + case 'email': + case 'isActive': + case 'isVies': + case 'isTaxDataChecked': + case 'isEqualizated': + case 'isFreezed': + case 'hasToInvoice': + case 'hasToInvoiceByAddress': + case 'isToBeMailed': + case 'hasSepaVnl': + case 'hasLcr': + case 'hasCoreVnl': + case 'countryFk': + case 'provinceFk': + case 'salesPersonFk': + case 'businessTypeFk': + case 'payMethodFk': + case 'sageTaxTypeFk': + case 'sageTransactionTypeFk': + 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]; + } + + preview(client) { + this.clientSelected = client; + this.$.preview.show(); + } +} + +ngModule.vnComponent('vnClientExtendedList', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/client/front/extended-list/locale/es.yml b/modules/client/front/extended-list/locale/es.yml new file mode 100644 index 000000000..ea56f3af2 --- /dev/null +++ b/modules/client/front/extended-list/locale/es.yml @@ -0,0 +1,3 @@ +Mailing: Env. emails +Sage tr. type: Tipo tr. sage +Yes: Sí \ No newline at end of file diff --git a/modules/client/front/extended-list/style.scss b/modules/client/front/extended-list/style.scss new file mode 100644 index 000000000..7625b5d16 --- /dev/null +++ b/modules/client/front/extended-list/style.scss @@ -0,0 +1,6 @@ +@import "variables"; + +vn-chip.success, +vn-chip.alert { + color: $color-font-bg +} \ No newline at end of file diff --git a/modules/client/front/index.js b/modules/client/front/index.js index ea732beea..a5782c789 100644 --- a/modules/client/front/index.js +++ b/modules/client/front/index.js @@ -47,3 +47,4 @@ import './consumption-search-panel'; import './defaulter'; import './notification'; import './unpaid'; +import './extended-list'; diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index 4eb99318c..de4b91e0b 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -33,6 +33,7 @@ Search client by id or name: Buscar clientes por identificador o nombre # Sections Clients: Clientes +Extended list: Listado extendido Defaulter: Morosos New client: Nuevo cliente Fiscal data: Datos fiscales diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json index 293243470..6616443bb 100644 --- a/modules/client/front/routes.json +++ b/modules/client/front/routes.json @@ -7,6 +7,7 @@ "menus": { "main": [ {"state": "client.index", "icon": "person"}, + {"state": "client.extendedList", "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": "/extended-list", + "state": "client.extendedList", + "component": "vn-client-extended-list", + "description": "Extended list" } ] } diff --git a/modules/client/front/search-panel/locale/es.yml b/modules/client/front/search-panel/locale/es.yml index 93d2faf53..b0d0649c8 100644 --- a/modules/client/front/search-panel/locale/es.yml +++ b/modules/client/front/search-panel/locale/es.yml @@ -1,7 +1,7 @@ Client id: Id cliente Tax number: NIF/CIF Name: Nombre -Social name: Razon social +Social name: Razón social Town/City: Ciudad Postcode: Código postal Email: E-mail diff --git a/modules/monitor/front/index/tickets/index.js b/modules/monitor/front/index/tickets/index.js index 0770d8634..91d9079d8 100644 --- a/modules/monitor/front/index/tickets/index.js +++ b/modules/monitor/front/index/tickets/index.js @@ -53,7 +53,7 @@ export default class Controller extends Section { }, { field: 'shippedDate', - searchable: false + datepicker: true }, { field: 'theoreticalHour',