diff --git a/back/methods/user-config-view/save.js b/back/methods/user-config-view/save.js index da8a27083..b2144c01e 100644 --- a/back/methods/user-config-view/save.js +++ b/back/methods/user-config-view/save.js @@ -7,8 +7,7 @@ module.exports = function(Self) { required: true, description: `Code of the table you ask its configuration`, http: {source: 'body'} - } - ], + }], returns: { type: 'object', root: true @@ -29,6 +28,6 @@ module.exports = function(Self) { config.userFk = ctx.req.accessToken.userId; - return await Self.app.models.UserConfigView.create(config); + return Self.app.models.UserConfigView.create(config); }; }; diff --git a/back/model-config.json b/back/model-config.json index 18bf4cf98..8ad15a16a 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -29,6 +29,9 @@ "ChatConfig": { "dataSource": "vn" }, + "DefaultViewConfig": { + "dataSource": "vn" + }, "Delivery": { "dataSource": "vn" }, diff --git a/back/models/default-view-config.json b/back/models/default-view-config.json new file mode 100644 index 000000000..88164692d --- /dev/null +++ b/back/models/default-view-config.json @@ -0,0 +1,25 @@ +{ + "name": "DefaultViewConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "salix.defaultViewConfig" + } + }, + "properties": { + "tableCode": { + "id": true, + "type": "string", + "required": true + }, + "columns": { + "type": "object" + } + }, + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} diff --git a/db/changes/10380-allsaints/00-defaultViewConfig.sql b/db/changes/10380-allsaints/00-defaultViewConfig.sql new file mode 100644 index 000000000..e4b2f6c3d --- /dev/null +++ b/db/changes/10380-allsaints/00-defaultViewConfig.sql @@ -0,0 +1,14 @@ +CREATE TABLE `salix`.`defaultViewConfig` +( + tableCode VARCHAR(25) not null, + columns JSON not null +) +comment 'The default configuration of columns for views'; + +INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns) + VALUES + ('itemsIndex', '{"intrastat":false,"stemMultiplier":false,"landed":false}'), + ('latestBuys', '{"intrastat":false,"description":false,"density":false,"isActive":false,"freightValue":false,"packageValue":false,"isIgnored":false,"price2":false,"minPrice":true,"ektFk":false,"weight":false,"id":true,"packing":true,"grouping":true,"quantity":true,"size":false,"name":true,"code":true,"origin":true,"family":true,"entryFk":true,"buyingValue":true,"comissionValue":false,"price3":true,"packageFk":true,"packingOut":true}'), + ('ticketsMonitor', '{"id":false}'); + + \ No newline at end of file diff --git a/e2e/helpers/extensions.js b/e2e/helpers/extensions.js index 1539aca85..789c800b5 100644 --- a/e2e/helpers/extensions.js +++ b/e2e/helpers/extensions.js @@ -341,48 +341,32 @@ let actions = { }, waitForTextInElement: async function(selector, text) { - const expectedText = text.toLowerCase(); - return new Promise((resolve, reject) => { - let attempts = 0; - const interval = setInterval(async() => { - const currentText = await this.evaluate(selector => { - return document.querySelector(selector).innerText.toLowerCase(); - }, selector); - - if (currentText === expectedText || attempts === 40) { - clearInterval(interval); - resolve(currentText); - } - attempts += 1; - }, 100); - }).then(result => { - return expect(result).toContain(expectedText); - }); + await this.waitForFunction((selector, text) => { + if (document.querySelector(selector)) { + const innerText = document.querySelector(selector).innerText.toLowerCase(); + const expectedText = text.toLowerCase(); + if (innerText.includes(expectedText)) + return innerText; + } + }, {}, selector, text); }, waitForTextInField: async function(selector, text) { - let builtSelector = await this.selectorFormater(selector); - await this.waitForSelector(builtSelector); - const expectedText = text.toLowerCase(); - return new Promise((resolve, reject) => { - let attempts = 0; - const interval = setInterval(async() => { - const currentText = await this.evaluate(selector => { - return document.querySelector(selector).value.toLowerCase(); - }, builtSelector); + const builtSelector = await this.selectorFormater(selector); + const expectedValue = text.toLowerCase(); - if (currentText === expectedText || attempts === 40) { - clearInterval(interval); - resolve(currentText); + try { + await this.waitForFunction((selector, text) => { + const element = document.querySelector(selector); + if (element) { + const value = element.value.toLowerCase(); + if (value.includes(text)) + return true; } - attempts += 1; - }, 100); - }).then(result => { - if (result === '') - return expect(result).toEqual(expectedText); - - return expect(result).toContain(expectedText); - }); + }, {}, builtSelector, expectedValue); + } catch (error) { + throw new Error(`${text} wasn't the value of ${builtSelector}, ${error}`); + } }, selectorFormater: function(selector) { diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index d3e4da99a..d8ebaa069 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -313,27 +313,26 @@ export default { }, itemsIndex: { createItemButton: `vn-float-button`, - firstSearchResult: 'vn-item-index a:nth-child(1)', - searchResult: 'vn-item-index a.vn-tr', - firstResultPreviewButton: 'vn-item-index vn-tbody > :nth-child(1) .buttons > [icon="preview"]', + firstSearchResult: 'vn-item-index tbody tr:nth-child(1)', + searchResult: 'vn-item-index tbody tr:not(.empty-rows)', + firstResultPreviewButton: 'vn-item-index tbody > :nth-child(1) .buttons > [icon="preview"]', searchResultCloneButton: 'vn-item-index .buttons > [icon="icon-clone"]', acceptClonationAlertButton: '.vn-confirm.shown [response="accept"]', closeItemSummaryPreview: '.vn-popup.shown', - fieldsToShowButton: 'vn-item-index vn-table > div > div > vn-icon-button[icon="more_vert"]', - fieldsToShowForm: '.vn-popover.shown .content', - firstItemImage: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > img', - firstItemImageTd: 'vn-item-index vn-table a:nth-child(1) vn-td:nth-child(1)', - firstItemId: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(2)', - idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(1) > vn-check', - stemsCheckbox: '.vn-popover.shown vn-horizontal:nth-child(2) > vn-check', - sizeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check', - typeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(5) > vn-check', - categoryCheckbox: '.vn-popover.shown vn-horizontal:nth-child(6) > vn-check', - intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(7) > vn-check', - originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(8) > vn-check', - buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(9) > vn-check', - destinyCheckbox: '.vn-popover.shown vn-horizontal:nth-child(10) > vn-check', - taxClassCheckbox: '.vn-popover.shown vn-horizontal:nth-child(11) > vn-check', + shownColumns: 'vn-item-index vn-button[id="shownColumns"]', + shownColumnsList: '.vn-popover.shown .content', + firstItemImage: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(1) > img', + firstItemImageTd: 'vn-item-index smart-table tr:nth-child(1) td:nth-child(1)', + firstItemId: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(2)', + idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Identifier"]', + stemsCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Stems"]', + sizeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Size"]', + typeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Type"]', + categoryCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Category"]', + intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]', + originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]', + buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]', + densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]', saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button' }, itemFixedPrice: { @@ -1087,7 +1086,7 @@ export default { allBuyCheckbox: 'vn-entry-buy-index thead vn-check', firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check', deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]', - addBuyButton: 'vn-entry-buy-index vn-icon[icon="add_circle"]', + addBuyButton: 'vn-entry-buy-index vn-icon[icon="add"]', secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]', secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]', secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]', @@ -1109,9 +1108,9 @@ export default { importBuysButton: 'vn-entry-buy-import button[type="submit"]' }, entryLatestBuys: { - firstBuy: 'vn-entry-latest-buys vn-tbody > a:nth-child(1)', - allBuysCheckBox: 'vn-entry-latest-buys vn-thead vn-check', - secondBuyCheckBox: 'vn-entry-latest-buys a:nth-child(2) vn-check[ng-model="buy.checked"]', + firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)', + allBuysCheckBox: 'vn-entry-latest-buys thead vn-check', + secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.$checked"]', editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]', fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]', newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]', diff --git a/e2e/paths/01-salix/01_login.spec.js b/e2e/paths/01-salix/01_login.spec.js index 7414856da..9dba61379 100644 --- a/e2e/paths/01-salix/01_login.spec.js +++ b/e2e/paths/01-salix/01_login.spec.js @@ -19,7 +19,9 @@ describe('Login path', async() => { const message = await page.waitForSnackbar(); const state = await page.getState(); - expect(message.text).toContain('Invalid login, remember that distinction is made between uppercase and lowercase'); + const errorMessage = 'Invalid login, remember that distinction is made between uppercase and lowercase'; + + expect(message.text).toContain(errorMessage); expect(state).toBe('login'); }); @@ -28,7 +30,9 @@ describe('Login path', async() => { const message = await page.waitForSnackbar(); const state = await page.getState(); - expect(message.text).toContain('Invalid login, remember that distinction is made between uppercase and lowercase'); + const errorMessage = 'Invalid login, remember that distinction is made between uppercase and lowercase'; + + expect(message.text).toContain(errorMessage); expect(state).toBe('login'); }); diff --git a/e2e/paths/02-client/03_edit_fiscal_data.spec.js b/e2e/paths/02-client/03_edit_fiscal_data.spec.js index ab0a61ddc..4ae1d4eca 100644 --- a/e2e/paths/02-client/03_edit_fiscal_data.spec.js +++ b/e2e/paths/02-client/03_edit_fiscal_data.spec.js @@ -112,7 +112,7 @@ describe('Client Edit fiscalData path', () => { expect(message.text).toContain('Cannot check Equalization Tax in this NIF/CIF'); }); - it('should finally edit the fixcal data correctly as VIES isnt checked and fiscal id is valid for EQtax', async() => { + it('should edit the fiscal data correctly as VIES isnt checked and fiscal id is valid for EQtax', async() => { await page.clearInput(selectors.clientFiscalData.fiscalId); await page.write(selectors.clientFiscalData.fiscalId, '94980061C'); await page.waitToClick(selectors.clientFiscalData.saveButton); diff --git a/e2e/paths/02-client/04_edit_billing_data.spec.js b/e2e/paths/02-client/04_edit_billing_data.spec.js index 6bc48093e..de3270f93 100644 --- a/e2e/paths/02-client/04_edit_billing_data.spec.js +++ b/e2e/paths/02-client/04_edit_billing_data.spec.js @@ -38,10 +38,13 @@ describe('Client Edit billing data path', () => { await page.autocompleteSearch(selectors.clientBillingData.newBankEntityCountry, 'España'); await page.write(selectors.clientBillingData.newBankEntityCode, '9999'); await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton); + const message = await page.waitForSnackbar(); await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank'); const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value'); expect(newcode).toEqual('GTHMCT Gotham City Bank'); + + expect(message.text).toContain('Data saved!'); }); it(`should confirm the IBAN pay method was sucessfully saved`, async() => { diff --git a/e2e/paths/04-item/01_summary.spec.js b/e2e/paths/04-item/01_summary.spec.js index a7526accb..e24fa6a9f 100644 --- a/e2e/paths/04-item/01_summary.spec.js +++ b/e2e/paths/04-item/01_summary.spec.js @@ -16,13 +16,13 @@ describe('Item summary path', () => { it('should search for an item', async() => { await page.doSearch('Ranged weapon'); - const nResults = await page.countElement(selectors.itemsIndex.searchResult); + const resultsCount = await page.countElement(selectors.itemsIndex.searchResult); await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Ranged weapon'); await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); const isVisible = await page.isVisible(selectors.itemSummary.basicData); - expect(nResults).toBe(3); + expect(resultsCount).toBe(3); expect(isVisible).toBeTruthy(); }); @@ -61,12 +61,12 @@ describe('Item summary path', () => { it('should search for other item', async() => { await page.doSearch('Melee Reinforced'); - const nResults = await page.countElement(selectors.itemsIndex.searchResult); + const resultsCount = await page.countElement(selectors.itemsIndex.searchResult); await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); await page.waitForSelector(selectors.itemSummary.basicData, {visible: true}); - expect(nResults).toBe(2); + expect(resultsCount).toBe(2); }); it(`should now check the item summary preview shows fields from basic data`, async() => { diff --git a/e2e/paths/04-item/07_create.spec.js b/e2e/paths/04-item/07_create.spec.js new file mode 100644 index 000000000..0820f2db7 --- /dev/null +++ b/e2e/paths/04-item/07_create.spec.js @@ -0,0 +1,71 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item Create', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should search for the item Infinity Gauntlet to confirm it isn't created yet`, async() => { + await page.doSearch('Infinity Gauntlet'); + const resultsCount = await page.countElement(selectors.itemsIndex.searchResult); + + expect(resultsCount).toEqual(0); + }); + + it('should access to the create item view by clicking the create floating button', async() => { + await page.waitToClick(selectors.itemsIndex.createItemButton); + await page.waitForState('item.create'); + }); + + it('should return to the item index by clickig the cancel button', async() => { + await page.waitToClick(selectors.itemCreateView.cancelButton); + await page.waitForState('item.index'); + }); + + it('should now access to the create item view by clicking the create floating button', async() => { + await page.waitToClick(selectors.itemsIndex.createItemButton); + await page.waitForState('item.create'); + }); + + it('should create the Infinity Gauntlet item', async() => { + await page.write(selectors.itemCreateView.temporalName, 'Infinity Gauntlet'); + await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo'); + await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares'); + await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand'); + await page.waitToClick(selectors.itemCreateView.createButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm Infinity Gauntlet item was created', async() => { + let result = await page + .waitToGetProperty(selectors.itemBasicData.name, 'value'); + + expect(result).toEqual('Infinity Gauntlet'); + + result = await page + .waitToGetProperty(selectors.itemBasicData.type, 'value'); + + expect(result).toEqual('Crisantemo'); + + result = await page + .waitToGetProperty(selectors.itemBasicData.intrastat, 'value'); + + expect(result).toEqual('5080000 Coral y materiales similares'); + + result = await page + .waitToGetProperty(selectors.itemBasicData.origin, 'value'); + + expect(result).toEqual('Holand'); + }); +}); diff --git a/e2e/paths/04-item/07_create_and_clone.spec.js b/e2e/paths/04-item/07_create_and_clone.spec.js deleted file mode 100644 index 938f15e3f..000000000 --- a/e2e/paths/04-item/07_create_and_clone.spec.js +++ /dev/null @@ -1,105 +0,0 @@ -import selectors from '../../helpers/selectors.js'; -import getBrowser from '../../helpers/puppeteer'; - -describe('Item Create/Clone path', () => { - let browser; - let page; - beforeAll(async() => { - browser = await getBrowser(); - page = browser.page; - await page.loginAndModule('buyer', 'item'); - }); - - afterAll(async() => { - await browser.close(); - }); - - describe('create', () => { - it(`should search for the item Infinity Gauntlet to confirm it isn't created yet`, async() => { - await page.doSearch('Infinity Gauntlet'); - const nResults = await page.countElement(selectors.itemsIndex.searchResult); - - expect(nResults).toEqual(0); - }); - - it('should access to the create item view by clicking the create floating button', async() => { - await page.waitToClick(selectors.itemsIndex.createItemButton); - await page.waitForState('item.create'); - }); - - it('should return to the item index by clickig the cancel button', async() => { - await page.waitToClick(selectors.itemCreateView.cancelButton); - await page.waitForState('item.index'); - }); - - it('should now access to the create item view by clicking the create floating button', async() => { - await page.waitToClick(selectors.itemsIndex.createItemButton); - await page.waitForState('item.create'); - }); - - it('should create the Infinity Gauntlet item', async() => { - await page.write(selectors.itemCreateView.temporalName, 'Infinity Gauntlet'); - await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo'); - await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares'); - await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand'); - await page.waitToClick(selectors.itemCreateView.createButton); - const message = await page.waitForSnackbar(); - - expect(message.text).toContain('Data saved!'); - }); - - it('should confirm Infinity Gauntlet item was created', async() => { - let result = await page - .waitToGetProperty(selectors.itemBasicData.name, 'value'); - - expect(result).toEqual('Infinity Gauntlet'); - - result = await page - .waitToGetProperty(selectors.itemBasicData.type, 'value'); - - expect(result).toEqual('Crisantemo'); - - result = await page - .waitToGetProperty(selectors.itemBasicData.intrastat, 'value'); - - expect(result).toEqual('5080000 Coral y materiales similares'); - - result = await page - .waitToGetProperty(selectors.itemBasicData.origin, 'value'); - - expect(result).toEqual('Holand'); - }); - }); - - // Issue #2201 - // When there is just one result you're redirected automatically to it, so - // it's not possible to use the clone option. - xdescribe('clone', () => { - it('should return to the items index by clicking the return to items button', async() => { - await page.waitToClick(selectors.itemBasicData.goToItemIndexButton); - await page.waitForSelector(selectors.itemsIndex.createItemButton); - await page.waitForState('item.index'); - }); - - it(`should search for the item Infinity Gauntlet`, async() => { - await page.doSearch('Infinity Gauntlet'); - const nResults = await page.countElement(selectors.itemsIndex.searchResult); - - expect(nResults).toEqual(1); - }); - - it(`should clone the Infinity Gauntlet`, async() => { - await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Infinity Gauntlet'); - await page.waitToClick(selectors.itemsIndex.searchResultCloneButton); - await page.waitToClick(selectors.itemsIndex.acceptClonationAlertButton); - await page.waitForState('item.tags'); - }); - - it('should search for the item Infinity Gauntlet and find two', async() => { - await page.doSearch('Infinity Gauntlet'); - const nResults = await page.countElement(selectors.itemsIndex.searchResult); - - expect(nResults).toEqual(2); - }); - }); -}); diff --git a/e2e/paths/04-item/09_index.spec.js b/e2e/paths/04-item/09_index.spec.js index 262627e99..f9262863d 100644 --- a/e2e/paths/04-item/09_index.spec.js +++ b/e2e/paths/04-item/09_index.spec.js @@ -16,8 +16,8 @@ describe('Item index path', () => { }); it('should click on the fields to show button to open the list of columns to show', async() => { - await page.waitToClick(selectors.itemsIndex.fieldsToShowButton); - const visible = await page.isVisible(selectors.itemsIndex.fieldsToShowForm); + await page.waitToClick(selectors.itemsIndex.shownColumns); + const visible = await page.isVisible(selectors.itemsIndex.shownColumnsList); expect(visible).toBeTruthy(); }); @@ -31,7 +31,7 @@ describe('Item index path', () => { await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); await page.waitToClick(selectors.itemsIndex.originCheckbox); await page.waitToClick(selectors.itemsIndex.buyerCheckbox); - await page.waitToClick(selectors.itemsIndex.destinyCheckbox); + await page.waitToClick(selectors.itemsIndex.densityCheckbox); await page.waitToClick(selectors.itemsIndex.saveFieldsButton); const message = await page.waitForSnackbar(); @@ -39,6 +39,7 @@ describe('Item index path', () => { }); it('should navigate forth and back to see the images column is still visible', async() => { + await page.closePopup(); await page.waitToClick(selectors.itemsIndex.firstSearchResult); await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); await page.waitToClick(selectors.globalItems.searchButton); @@ -54,7 +55,7 @@ describe('Item index path', () => { }); it('should mark all unchecked boxes to leave the index as it was', async() => { - await page.waitToClick(selectors.itemsIndex.fieldsToShowButton); + await page.waitToClick(selectors.itemsIndex.shownColumns); await page.waitToClick(selectors.itemsIndex.idCheckbox); await page.waitToClick(selectors.itemsIndex.stemsCheckbox); await page.waitToClick(selectors.itemsIndex.sizeCheckbox); @@ -63,7 +64,7 @@ describe('Item index path', () => { await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); await page.waitToClick(selectors.itemsIndex.originCheckbox); await page.waitToClick(selectors.itemsIndex.buyerCheckbox); - await page.waitToClick(selectors.itemsIndex.destinyCheckbox); + await page.waitToClick(selectors.itemsIndex.densityCheckbox); await page.waitToClick(selectors.itemsIndex.saveFieldsButton); const message = await page.waitForSnackbar(); @@ -71,6 +72,7 @@ describe('Item index path', () => { }); it('should now navigate forth and back to see the ids column is now visible', async() => { + await page.closePopup(); await page.waitToClick(selectors.itemsIndex.firstSearchResult); await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); await page.waitToClick(selectors.globalItems.searchButton); diff --git a/e2e/paths/05-ticket/04_packages.spec.js b/e2e/paths/05-ticket/04_packages.spec.js index 06720ed7a..f874307a8 100644 --- a/e2e/paths/05-ticket/04_packages.spec.js +++ b/e2e/paths/05-ticket/04_packages.spec.js @@ -62,7 +62,7 @@ describe('Ticket Create packages path', () => { expect(result).toEqual('7 : Container medical box 1m'); }); - it(`should confirm the first quantity is just a number and the string part was ignored by the imput number`, async() => { + it(`should confirm quantity is just a number and the string part was ignored by the imput number`, async() => { await page.waitForTextInField(selectors.ticketPackages.firstQuantity, '-99'); const result = await page.waitToGetProperty(selectors.ticketPackages.firstQuantity, 'value'); diff --git a/e2e/paths/12-entry/03_latestBuys.spec.js b/e2e/paths/12-entry/03_latestBuys.spec.js index f7dc07ca9..553d41b95 100644 --- a/e2e/paths/12-entry/03_latestBuys.spec.js +++ b/e2e/paths/12-entry/03_latestBuys.spec.js @@ -31,7 +31,7 @@ describe('Entry lastest buys path', () => { await page.waitForSelector(selectors.entryLatestBuys.fieldAutocomplete, {visible: true}); }); - it('should search for the "Description" field and type a new description for the items in each selected buy', async() => { + it('should search for the "Description" and type a new one for the items in each selected buy', async() => { await page.autocompleteSearch(selectors.entryLatestBuys.fieldAutocomplete, 'Description'); await page.write(selectors.entryLatestBuys.newValueInput, 'Crafted item'); await page.waitToClick(selectors.entryLatestBuys.acceptEditBuysDialog); diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js index 4042c99b6..a39e88ce6 100644 --- a/e2e/paths/12-entry/07_buys.spec.js +++ b/e2e/paths/12-entry/07_buys.spec.js @@ -28,7 +28,7 @@ describe('Entry import, create and edit buys path', () => { await page.waitForState('entry.card.buy.import'); }); - it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { + it('should fill the form, import the a JSON file and select items for each import and confirm import', async() => { let currentDir = process.cwd(); let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; @@ -42,7 +42,8 @@ describe('Entry import, create and edit buys path', () => { await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846'); await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); - await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); + const itemName = 'Melee Reinforced weapon heavy shield 1x0.5m'; + await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, itemName); await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m'); @@ -88,37 +89,37 @@ describe('Entry import, create and edit buys path', () => { it('should edit the newest buy', async() => { await page.clearInput(selectors.entryBuys.secondBuyPackingPrice); - await page.waitForTextInField(selectors.entryBuys.secondBuyPackingPrice, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyPackingPrice, '100'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice); - await page.waitForTextInField(selectors.entryBuys.secondBuyGroupingPrice, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyPrice); - await page.waitForTextInField(selectors.entryBuys.secondBuyPrice, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyPrice, '300'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyGrouping); - await page.waitForTextInField(selectors.entryBuys.secondBuyGrouping, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyGrouping, '400'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyPacking); - await page.waitForTextInField(selectors.entryBuys.secondBuyPacking, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyPacking, '500'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyWeight); - await page.waitForTextInField(selectors.entryBuys.secondBuyWeight, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyWeight, '600'); await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyStickers); - await page.waitForTextInField(selectors.entryBuys.secondBuyStickers, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyStickers, '700'); await page.waitForSnackbar(); @@ -126,7 +127,7 @@ describe('Entry import, create and edit buys path', () => { await page.waitForSnackbar(); await page.clearInput(selectors.entryBuys.secondBuyQuantity); - await page.waitForTextInField(selectors.entryBuys.secondBuyQuantity, ''); + await page.waitForTimeout(250); await page.write(selectors.entryBuys.secondBuyQuantity, '800'); }); diff --git a/front/core/components/contextmenu/index.js b/front/core/components/contextmenu/index.js index 646df1a0a..fa1db6887 100755 --- a/front/core/components/contextmenu/index.js +++ b/front/core/components/contextmenu/index.js @@ -49,7 +49,7 @@ export default class Contextmenu { get rowIndex() { if (!this.row) return null; - const table = this.row.closest('vn-table, .vn-table'); + const table = this.row.closest('table, vn-table, .vn-table'); const rows = table.querySelectorAll('[ng-repeat]'); return Array.from(rows).findIndex( @@ -67,13 +67,13 @@ export default class Contextmenu { get cell() { if (!this.target) return null; - return this.target.closest('vn-td, .vn-td, vn-td-editable'); + return this.target.closest('td, vn-td, .vn-td, vn-td-editable'); } get cellIndex() { if (!this.row) return null; - const cells = this.row.querySelectorAll('vn-td, .vn-td, vn-td-editable'); + const cells = this.row.querySelectorAll('td, vn-td, .vn-td, vn-td-editable'); return Array.from(cells).findIndex( cellItem => cellItem == this.cell ); @@ -82,8 +82,8 @@ export default class Contextmenu { get rowHeader() { if (!this.row) return null; - const table = this.row.closest('vn-table, .vn-table'); - const headerCells = table && table.querySelectorAll('vn-thead vn-th'); + const table = this.row.closest('table, vn-table, .vn-table'); + const headerCells = table && table.querySelectorAll('thead th, vn-thead vn-th'); const headerCell = headerCells && headerCells[this.cellIndex]; return headerCell; @@ -147,7 +147,7 @@ export default class Contextmenu { */ isActionAllowed() { if (!this.target) return false; - const isTableCell = this.target.closest('vn-td, .vn-td'); + const isTableCell = this.target.closest('td, vn-td, .vn-td'); return isTableCell && this.fieldName; } @@ -172,9 +172,28 @@ export default class Contextmenu { excludeSelection() { let where = {[this.fieldName]: {neq: this.fieldValue}}; if (this.exprBuilder) { - where = buildFilter(where, (param, value) => - this.exprBuilder({param, value}) - ); + where = {[this.fieldName]: this.fieldValue}; + where = buildFilter(where, (param, value) => { + const expr = this.exprBuilder({param, value}); + const props = Object.keys(expr); + let newExpr = {}; + for (let prop of props) { + if (expr[prop].like) { + const operator = expr[prop].like; + newExpr[prop] = {nlike: operator}; + } else if (expr[prop].between) { + const operator = expr[prop].between; + newExpr = { + or: [ + {[prop]: {lt: operator[0]}}, + {[prop]: {gt: operator[1]}}, + ] + }; + } else + newExpr[prop] = {neq: this.fieldValue}; + } + return newExpr; + }); } this.model.addFilter({where}); @@ -208,15 +227,22 @@ export default class Contextmenu { if (prop == findProp) delete instance[prop]; - if (prop === 'and') { - for (let [index, param] of instance[prop].entries()) { + if (prop === 'and' || prop === 'or') { + const instanceCopy = instance[prop].slice(); + for (let param of instanceCopy) { const [key] = Object.keys(param); + const index = instance[prop].findIndex(param => { + return Object.keys(param)[0] == key; + }); if (key == findProp) instance[prop].splice(index, 1); if (param[key] instanceof Array) removeProp(param, filterKey, key); } + + if (instance[prop].length == 0) + delete instance[prop]; } } diff --git a/front/core/components/index.js b/front/core/components/index.js index 3ccc64b89..86ab89212 100644 --- a/front/core/components/index.js +++ b/front/core/components/index.js @@ -52,3 +52,4 @@ import './wday-picker'; import './datalist'; import './contextmenu'; import './rating'; +import './smart-table'; diff --git a/front/core/components/multi-check/multi-check.js b/front/core/components/multi-check/multi-check.js index d8fda6404..afa1bc3c4 100644 --- a/front/core/components/multi-check/multi-check.js +++ b/front/core/components/multi-check/multi-check.js @@ -145,9 +145,8 @@ export default class MultiCheck extends FormInput { toggle() { const data = this.model.data; if (!data) return; - data.forEach(el => { + for (let el of data) el[this.checkField] = this.checkAll; - }); } } @@ -156,8 +155,9 @@ ngModule.vnComponent('vnMultiCheck', { controller: MultiCheck, bindings: { model: '<', - checkField: ' + +
+ + +
+
+
+
+ {{model.data.length}} + results +
+ + +
+ + + + + + + + +
+ + +
+
+
+ + + + + + + + + + + +
+ +
Shown columns
+ +
+
+ + +
+ + + + + + + + + +
+
+
\ No newline at end of file diff --git a/front/core/components/smart-table/index.js b/front/core/components/smart-table/index.js new file mode 100644 index 000000000..f32d8aa4f --- /dev/null +++ b/front/core/components/smart-table/index.js @@ -0,0 +1,454 @@ +import ngModule from '../../module'; +import Component from '../../lib/component'; +import {buildFilter} from 'vn-loopback/util/filter'; +import angular from 'angular'; +import {camelToKebab} from '../../lib/string'; +import './style.scss'; +import './table.scss'; + +export default class SmartTable extends Component { + constructor($element, $, $transclude) { + super($element, $); + this.currentUserId = window.localStorage.currentUserWorkerId; + this.$transclude = $transclude; + this.sortCriteria = []; + this.$inputsScope; + this.columns = []; + this.autoSave = false; + this.transclude(); + } + + $onDestroy() { + const styleElement = document.querySelector('style[id="smart-table"]'); + if (this.$.css && styleElement) + styleElement.parentNode.removeChild(styleElement); + } + + get options() { + return this._options; + } + + set options(options) { + this._options = options; + if (!options) return; + + const activeButtons = options.activeButtons; + const missingId = activeButtons && activeButtons.shownColumns && !this.viewConfigId; + if (missingId) + throw new Error('vnSmartTable: View identifier not defined'); + } + + get model() { + return this._model; + } + + set model(value) { + this._model = value; + if (value) + this.$.model = value; + } + + get viewConfigId() { + return this._viewConfigId; + } + + set viewConfigId(value) { + this._viewConfigId = value; + + /* if (value) { + this.defaultViewConfig = {}; + + const url = 'DefaultViewConfigs'; + const filter = {where: {tableCode: value}}; + this.$http.get(url, {filter}) + .then(res => { + if (res && res.data.length) { + const columns = res.data[0].columns; + this.defaultViewConfig = columns; + } + }); + } */ + } + + getDefaultViewConfig() { + const url = 'DefaultViewConfigs'; + const filter = {where: {tableCode: this.viewConfigId}}; + return this.$http.get(url, {filter}) + .then(res => { + if (res && res.data.length) + return res.data[0].columns; + }); + } + + get viewConfig() { + return this._viewConfig; + } + + set viewConfig(value) { + this._viewConfig = value; + + if (!value) return; + + if (!value.length) { + this.getDefaultViewConfig().then(columns => { + const defaultViewConfig = columns ? columns : {}; + + const userViewModel = this.$.userViewModel; + for (const column of this.columns) { + if (defaultViewConfig[column.field] == undefined) + defaultViewConfig[column.field] = true; + } + + userViewModel.insert({ + userFk: this.currentUserId, + tableConfig: this.viewConfigId, + configuration: defaultViewConfig + }); + }).finally(() => this.applyViewConfig()); + } else + this.applyViewConfig(); + } + + get checkedRows() { + const model = this.model; + if (model && model.data) + return model.data.filter(row => row.$checked); + + return null; + } + + get checkAll() { + return this._checkAll; + } + + set checkAll(value) { + this._checkAll = value; + if (value !== undefined) { + const shownColumns = this.viewConfig[0].configuration; + for (let param in shownColumns) + shownColumns[param] = value; + } + } + + transclude() { + const slotTable = this.element.querySelector('#table'); + this.$transclude($clone => { + const table = $clone[0]; + slotTable.appendChild(table); + this.registerColumns(); + this.emptyDataRows(); + }, null, 'table'); + } + + saveViewConfig() { + const userViewModel = this.$.userViewModel; + const [viewConfig] = userViewModel.data; + viewConfig.configuration = Object.assign({}, viewConfig.configuration); + userViewModel.save() + .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) + .then(() => this.applyViewConfig()) + .then(() => this.$.smartTableColumns.hide()); + } + + applyViewConfig() { + const userViewModel = this.$.userViewModel; + const [viewConfig] = userViewModel.data; + + const selectors = []; + for (const column of this.columns) { + if (viewConfig.configuration[column.field] == false) { + const baseSelector = `smart-table[view-config-id="${this.viewConfigId}"] table`; + selectors.push(`${baseSelector} thead > tr > th:nth-child(${column.index + 1})`); + selectors.push(`${baseSelector} tbody > tr > td:nth-child(${column.index + 1})`); + } + } + + let styleElement = document.querySelector('style[id="smart-table"]'); + + if (styleElement) + styleElement.parentNode.removeChild(styleElement); + + if (selectors.length) { + const rule = selectors.join(', ') + '{display: none}'; + this.$.css = document.createElement('style'); + this.$.css.setAttribute('id', 'smart-table'); + document.head.appendChild(this.$.css); + this.$.css.appendChild(document.createTextNode(rule)); + } + } + + registerColumns() { + const header = this.element.querySelector('thead > tr'); + if (!header) return; + const columns = header.querySelectorAll('th'); + + // Click handler + for (const [index, column] of columns.entries()) { + const field = column.getAttribute('field'); + if (field) { + const columnElement = angular.element(column); + const caption = columnElement.text().trim(); + + this.columns.push({field, caption, index}); + + column.addEventListener('click', () => this.orderHandler(column)); + } + } + } + + emptyDataRows() { + const header = this.element.querySelector('thead > tr'); + const columns = header.querySelectorAll('th'); + const tbody = this.element.querySelector('tbody'); + if (tbody) { + const noSearch = this.$compile(` + + Enter a new search + + `)(this.$); + tbody.appendChild(noSearch[0]); + + const noRows = this.$compile(` + + No data + + `)(this.$); + tbody.appendChild(noRows[0]); + } + } + + orderHandler(element) { + const field = element.getAttribute('field'); + const existingCriteria = this.sortCriteria.find(criteria => { + return criteria.field == field; + }); + + const isASC = existingCriteria && existingCriteria.sortType == 'ASC'; + const isDESC = existingCriteria && existingCriteria.sortType == 'DESC'; + + if (!existingCriteria) { + this.sortCriteria.push({field: field, sortType: 'ASC'}); + element.classList.remove('desc'); + element.classList.add('asc'); + } + + if (isDESC) { + this.sortCriteria.splice(this.sortCriteria.findIndex(criteria => { + return criteria.field == field; + }), 1); + element.classList.remove('desc'); + element.classList.remove('asc'); + } + + if (isASC) { + existingCriteria.sortType = 'DESC'; + element.classList.remove('asc'); + element.classList.add('desc'); + } + + this.applySort(); + } + + displaySearch() { + const header = this.element.querySelector('thead > tr'); + if (!header) return; + + const tbody = this.element.querySelector('tbody'); + const columns = header.querySelectorAll('th'); + + const hasSearchRow = tbody.querySelector('tr#searchRow'); + if (hasSearchRow) { + if (this.$inputsScope) + this.$inputsScope.$destroy(); + + return hasSearchRow.remove(); + } + + const searchRow = document.createElement('tr'); + searchRow.setAttribute('id', 'searchRow'); + + this.$inputsScope = this.$.$new(); + + for (let column of columns) { + const field = column.getAttribute('field'); + const cell = document.createElement('td'); + if (field) { + let input; + let options; + const columnOptions = this.options && this.options.columns; + + if (columnOptions) + options = columnOptions.find(column => column.field == field); + + if (options && options.searchable == false) { + searchRow.appendChild(cell); + continue; + } + + if (options && options.autocomplete) { + let props = ``; + + const autocomplete = options.autocomplete; + for (const prop in autocomplete) + props += `${camelToKebab(prop)}="${autocomplete[prop]}"\n`; + input = this.$compile(` + `)(this.$inputsScope); + } else { + input = this.$compile(` + `)(this.$inputsScope); + } + cell.appendChild(input[0]); + } + searchRow.appendChild(cell); + } + + tbody.prepend(searchRow); + } + + searchWithEvent($event, field) { + if ($event.key != 'Enter') return; + + this.searchByColumn(field); + } + + searchByColumn(field) { + const searchCriteria = this.$inputsScope.searchProps[field]; + const emptySearch = 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(); + } + + addFilter(field, value) { + let where = {[field]: value}; + + if (this.exprBuilder) { + where = buildFilter(where, (param, value) => + this.exprBuilder({param, value}) + ); + } + + this.model.addFilter({where}); + } + + applySort() { + let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`); + order = order.join(', '); + + if (order) + this.model.order = order; + + this.model.refresh(); + } + + filterSanitizer(field) { + const userFilter = this.model.userFilter; + const userParams = this.model.userParams; + const where = userFilter && userFilter.where; + + if (this.exprBuilder) { + const param = this.exprBuilder({ + param: field, + value: null + }); + if (param) [field] = Object.keys(param); + } + + if (!where) return; + + const whereKeys = Object.keys(where); + for (let key of whereKeys) { + removeProp(where, field, key); + + if (!Object.keys(where)) + delete userFilter.where; + } + + function removeProp(instance, findProp, prop) { + if (prop == findProp) + delete instance[prop]; + + if (prop === 'and') { + for (let [index, param] of instance[prop].entries()) { + const [key] = Object.keys(param); + if (key == findProp) + instance[prop].splice(index, 1); + + if (param[key] instanceof Array) + removeProp(param, field, key); + } + } + } + + return {userFilter, userParams}; + } + + removeFilter() { + this.model.applyFilter(userFilter, userParams); + } + + createRow() { + let data = {}; + + if (this.defaultNewData) + data = this.defaultNewData(); + + this.model.insert(data); + } + + deleteAll() { + for (let row of this.checkedRows) + this.model.removeRow(row); + + if (this.autoSave) + this.saveAll(); + } + + saveAll() { + const model = this.model; + + if (!model.isChanged) + return this.vnApp.showError(this.$t('No changes to save')); + + this.model.save() + .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))); + } +} + +SmartTable.$inject = ['$element', '$scope', '$transclude']; + +ngModule.vnComponent('smartTable', { + template: require('./index.html'), + controller: SmartTable, + transclude: { + table: '?slotTable', + actions: '?slotActions' + }, + bindings: { + model: ' :before { + vertical-align: middle; + font-family: 'Material Icons'; + content: 'arrow_downward'; + color: $color-spacer; + margin-right: 2px; + opacity: 0 + + } + + &.asc > :before, &.desc > :before { + color: $color-font; + opacity: 1; + } + + &.asc > :before { + content: 'arrow_upward'; + } + + &.desc > :before { + content: 'arrow_downward'; + } + + &:hover > :before { + opacity: 1; + } + } + + th[field]:not([number]) { + & > :after { + vertical-align: middle; + font-family: 'Material Icons'; + content: 'arrow_downward'; + color: $color-spacer; + margin-left: 2px; + opacity: 0 + + } + + &.asc > :after, &.desc > :after { + color: $color-font; + opacity: 1; + } + + &.asc > :after { + content: 'arrow_upward'; + } + + &.desc > :after { + content: 'arrow_downward'; + } + + &:hover > :after { + opacity: 1; + } + } + + tr[vn-anchor] { + @extend %clickable; + } + + .totalRows { + color: $color-font-secondary; + } + + .actions-left, + .actions-right { + display: flex; + align-items: center; + + .button-group { + display: flex; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .3); + + & > vn-button { + box-shadow: 0 0 0 0 + } + } + } + + .actions-left { + justify-content: flex-start; + + slot-actions > vn-button, + & > vn-button, + .button-group { + margin-right: 10px + } + + slot-actions { + display: flex + } + } + + .actions-right { + justify-content: flex-end; + & > vn-button, + .button-group { + margin-left: 10px + } + } + + #table { + overflow-x: auto; + margin-top: 15px + } + + vn-tbody a[ng-repeat].vn-tr:focus { + background-color: $color-primary-light + } + + .new-row { + background-color: $color-success-light + } + + .changed-row { + background-color: $color-primary-light + } +} + +.smart-table-columns { + h6 { + color: $color-font-secondary + } + + & > vn-horizontal { + align-items: flex-start; + flex-wrap: wrap; + } + + vn-check { + flex: initial; + width: 33% + } +} \ No newline at end of file diff --git a/front/core/components/smart-table/table.scss b/front/core/components/smart-table/table.scss new file mode 100644 index 000000000..e0464465a --- /dev/null +++ b/front/core/components/smart-table/table.scss @@ -0,0 +1,108 @@ +@import "effects"; +@import "variables"; + +smart-table table { + width: 100%; + border-collapse: collapse; + + & > thead { + border-bottom: 2px solid $color-spacer; + + & > * > th { + font-weight: normal; + } + } + & > tfoot { + border-top: 2px solid $color-spacer; + } + thead, tbody, tfoot { + & > * { + & > th { + color: $color-font-light; + } + & > th, + & > td { + overflow: hidden; + } + & > th, + & > td { + text-align: left; + padding: 9px 5px; + white-space: nowrap; + text-overflow: ellipsis; + + &[number] { + text-align: right; + } + &[centered] { + text-align: center; + } + &[shrink] { + width: 1px; + text-align: center; + } + &[shrink-date] { + width: 100px; + max-width: 100px; + } + &[shrink-datetime] { + width: 150px; + max-width: 150px; + } + &[expand] { + max-width: 400px; + min-width: 0; + } + &[actions] { + width: 1px; + + & > * { + vertical-align: middle; + } + } + vn-icon.bright, i.bright { + color: #f7931e; + } + } + } + } + tbody > * { + border-bottom: 1px solid $color-spacer-light; + + &:last-child { + border-bottom: none; + } + & > td { + .chip { + padding: 4px; + border-radius: 4px; + color: $color-font-bg; + + &.notice { + background-color: $color-notice-medium + } + &.success { + background-color: $color-success-medium; + } + &.warning { + background-color: $color-main-medium; + } + &.alert { + background-color: $color-alert-medium; + } + &.message { + color: $color-font-dark; + background-color: $color-bg-dark + } + } + } + } + .vn-check { + margin: 0; + } + .empty-rows > td { + color: $color-font-secondary; + font-size: 1.375rem; + text-align: center; + } +} \ No newline at end of file diff --git a/front/core/directives/index.js b/front/core/directives/index.js index e0f42aef5..e77917285 100644 --- a/front/core/directives/index.js +++ b/front/core/directives/index.js @@ -16,3 +16,4 @@ import './droppable'; import './http-click'; import './http-submit'; import './anchor'; + diff --git a/front/salix/components/log/index.html b/front/salix/components/log/index.html index c78e00f44..0a0449038 100644 --- a/front/salix/components/log/index.html +++ b/front/salix/components/log/index.html @@ -1,5 +1,11 @@ - + diff --git a/gulpfile.js b/gulpfile.js index 59c79c154..102a8a0bf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -128,7 +128,8 @@ async function launchBackTest(done) { if (err) throw err; } -launchBackTest.description = `Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`; +launchBackTest.description = ` + Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`; // Backend tests diff --git a/modules/entry/front/buy/import/index.spec.js b/modules/entry/front/buy/import/index.spec.js index c948304c7..bf100dc83 100644 --- a/modules/entry/front/buy/import/index.spec.js +++ b/modules/entry/front/buy/import/index.spec.js @@ -63,8 +63,21 @@ describe('Entry', () => { } ]}`; const expectedBuys = [ - {'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, - {'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} + { + 'buyingValue': 5.77, + 'description': 'Bow', + 'grouping': 1, + 'packing': 1, + 'size': 1, + 'volume': 1200}, + + { + 'buyingValue': 2.16, + 'description': 'Arrow', + 'grouping': 1, + 'packing': 1, + 'size': 25, + 'volume': 1125} ]; controller.fillData(rawData); controller.$.$apply(); @@ -81,8 +94,21 @@ describe('Entry', () => { describe('fetchBuys()', () => { it(`should perform a query to fetch the buys data`, () => { const buys = [ - {'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, - {'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} + { + 'buyingValue': 5.77, + 'description': 'Bow', + 'grouping': 1, + 'packing': 1, + 'size': 1, + 'volume': 1200}, + + { + 'buyingValue': 2.16, + 'description': 'Arrow', + 'grouping': 1, + 'packing': 1, + 'size': 25, + 'volume': 1125} ]; const serializedParams = $httpParamSerializer({buys}); @@ -105,17 +131,31 @@ describe('Entry', () => { observation: '123456', ref: '1, 2', buys: [ - {'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, - {'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} + { + 'buyingValue': 5.77, + 'description': 'Bow', + 'grouping': 1, + 'packing': 1, + 'size': 1, + 'volume': 1200}, + { + 'buyingValue': 2.16, + 'description': 'Arrow', + 'grouping': 1, + 'packing': 1, + 'size': 25, + 'volume': 1125} ] }; controller.onSubmit(); - expect(controller.vnApp.showError).toHaveBeenCalledWith(`Some of the imported buys doesn't have an item`); + const message = `Some of the imported buys doesn't have an item`; + + expect(controller.vnApp.showError).toHaveBeenCalledWith(message); }); - it(`should perform a query to update columns`, () => { + it(`should now perform a query to update columns`, () => { jest.spyOn(controller.vnApp, 'showSuccess'); controller.$state.go = jest.fn(); @@ -123,8 +163,22 @@ describe('Entry', () => { observation: '123456', ref: '1, 2', buys: [ - {'itemFk': 10, 'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, - {'itemFk': 11, 'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} + { + 'itemFk': 10, + 'buyingValue': 5.77, + 'description': 'Bow', + 'grouping': 1, + 'packing': 1, + 'size': 1, + 'volume': 1200}, + { + 'itemFk': 11, + 'buyingValue': 2.16, + 'description': 'Arrow', + 'grouping': 1, + 'packing': 1, + 'size': 25, + 'volume': 1125} ] }; const params = controller.import; diff --git a/modules/entry/front/buy/index/index.html b/modules/entry/front/buy/index/index.html index dbe43c467..bb33b98b3 100644 --- a/modules/entry/front/buy/index/index.html +++ b/modules/entry/front/buy/index/index.html @@ -188,22 +188,19 @@ -
- - -
- + + - - - + - - - - - - - Picture - Id - Packing - Grouping - Quantity - Description - Size - Tags - Type - Intrastat - Origin - Density - Active - Family - Entry - Buying value - Freight value - Commission value - Package value - Is ignored - Grouping price - Packing price - Min price - Ekt - Weight - PackageName - PackingOut - - - - - - - - - - - - - - {{::buy.itemFk | zeroFill:6}} - - - - - {{::buy.packing | dashIfEmpty}} - - - - - {{::buy.grouping | dashIfEmpty}} - - - {{::buy.quantity}} - - {{::buy.description | dashIfEmpty}} - - {{::buy.size}} - -
- {{::buy.name}} - -

{{::buy.subName}}

-
-
- - -
- - {{::buy.code}} - - - {{::buy.intrastat}} - - {{::buy.origin}} - {{::buy.density}} - - - - - {{::buy.family}} - - - {{::buy.entryFk}} - - - {{::buy.buyingValue | currency: 'EUR':2}} - {{::buy.freightValue | currency: 'EUR':2}} - {{::buy.comissionValue | currency: 'EUR':2}} - {{::buy.packageValue | currency: 'EUR':2}} - - - - - {{::buy.price2 | currency: 'EUR':2}} - {{::buy.price3 | currency: 'EUR':2}} - {{::buy.minPrice | currency: 'EUR':2}} - {{::buy.ektFk | dashIfEmpty}} - {{::buy.weight}} - {{::buy.packageFk}} - {{::buy.packingOut}} -
- - - - + view-config-id="latestBuys" + options="$ctrl.smartTableOptions" + expr-builder="$ctrl.exprBuilder(param, value)"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Picture + Identifier + + Packing + + Grouping + + Quantity + + Description + + Size + + Tags + + Type + + Intrastat + + Origin + + Density + + Active + + Family + + Entry + + Buying value + + Freight value + + Commission value + + Package value + + Is ignored + + Grouping + + Packing + + Min + + Ekt + + Weight + + Package + + Package out +
+ + + + + + + {{::buy.itemFk}} + + + + {{::buy.packing | dashIfEmpty}} + + + + {{::buy.grouping | dashIfEmpty}} + + {{::buy.quantity}} + {{::buy.description | dashIfEmpty}} + {{::buy.size}} +
+ {{::buy.name}} + +

{{::buy.subName}}

+
+
+ + +
+ {{::buy.code}} + + {{::buy.intrastat}} + {{::buy.origin}}{{::buy.density}} + + + {{::buy.family}} + + {{::buy.entryFk}} + + {{::buy.buyingValue | currency: 'EUR':2}}{{::buy.freightValue | currency: 'EUR':2}}{{::buy.comissionValue | currency: 'EUR':2}}{{::buy.packageValue | currency: 'EUR':2}} + + + {{::buy.price2 | currency: 'EUR':2}}{{::buy.price3 | currency: 'EUR':2}}{{::buy.minPrice | currency: 'EUR':2}}{{::buy.ektFk | dashIfEmpty}}{{::buy.weight}}{{::buy.packageFk}}{{::buy.packingOut}}
+
+ +
{ beforeEach(ngModule('entry')); - beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpBackend_) => { + beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_) => { $httpBackend = _$httpBackend_; - let $element = $compile(' {}}, @@ -31,10 +31,10 @@ describe('Entry', () => { describe('get checked', () => { it(`should return a set of checked lines`, () => { controller.$.model.data = [ - {checked: true, id: 1}, - {checked: true, id: 2}, - {checked: true, id: 3}, - {checked: false, id: 4}, + {$checked: true, id: 1}, + {$checked: true, id: 2}, + {$checked: true, id: 3}, + {$checked: false, id: 4}, ]; let result = controller.checked; @@ -43,38 +43,10 @@ describe('Entry', () => { }); }); - describe('uncheck()', () => { - it(`should clear the selection of lines on the controller`, () => { - controller.$.model.data = [ - {checked: true, id: 1}, - {checked: true, id: 2}, - {checked: true, id: 3}, - {checked: false, id: 4}, - ]; - - let result = controller.checked; - - expect(result.length).toEqual(3); - - controller.uncheck(); - - result = controller.checked; - - expect(result.length).toEqual(0); - }); - }); - describe('onEditAccept()', () => { it(`should perform a query to update columns`, () => { - $httpBackend.whenGET('UserConfigViews/getConfig?tableCode=latestBuys').respond([]); - $httpBackend.whenGET('Buys/latestBuysFilter?filter=%7B%22limit%22:20%7D').respond([ - {entryFk: 1}, - {entryFk: 2}, - {entryFk: 3}, - {entryFk: 4} - ]); controller.editedColumn = {field: 'my field', newValue: 'the new value'}; - let query = 'Buys/editLatestBuys'; + const query = 'Buys/editLatestBuys'; $httpBackend.expectPOST(query).respond(); controller.onEditAccept(); diff --git a/modules/entry/front/latest-buys/locale/es.yml b/modules/entry/front/latest-buys/locale/es.yml index cb45724f8..21eae0307 100644 --- a/modules/entry/front/latest-buys/locale/es.yml +++ b/modules/entry/front/latest-buys/locale/es.yml @@ -14,4 +14,4 @@ Field to edit: Campo a editar PackageName: Cubo Edit: Editar buy(s): compra(s) -PackingOut: Packing envíos \ No newline at end of file +Package out: Embalaje envíos \ No newline at end of file diff --git a/modules/item/back/methods/item/filter.js b/modules/item/back/methods/item/filter.js index cff36a223..8cfefac9f 100644 --- a/modules/item/back/methods/item/filter.js +++ b/modules/item/back/methods/item/filter.js @@ -113,7 +113,7 @@ module.exports = Self => { return {'i.typeFk': value}; case 'categoryFk': return {'ic.id': value}; - case 'salesPersonFk': + case 'buyerFk': return {'it.workerFk': value}; case 'origin': return {'ori.code': value}; diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json index 6e171e9b0..d134d9283 100644 --- a/modules/item/back/model-config.json +++ b/modules/item/back/model-config.json @@ -23,6 +23,9 @@ "ItemCategory": { "dataSource": "vn" }, + "ItemFamily": { + "dataSource": "vn" + }, "ItemLog": { "dataSource": "vn" }, diff --git a/modules/item/back/models/intrastat.json b/modules/item/back/models/intrastat.json index e536e2581..18a964e7b 100644 --- a/modules/item/back/models/intrastat.json +++ b/modules/item/back/models/intrastat.json @@ -8,12 +8,12 @@ }, "properties": { "id": { - "type": "Number", + "type": "number", "id": true, "description": "Identifier" }, "description": { - "type": "String" + "type": "string" } }, "relations": { diff --git a/modules/item/back/models/item-category.json b/modules/item/back/models/item-category.json index fe6d804d8..89fb6d6c9 100644 --- a/modules/item/back/models/item-category.json +++ b/modules/item/back/models/item-category.json @@ -8,18 +8,18 @@ }, "properties": { "id": { - "type": "Number", + "type": "number", "id": true, "description": "Identifier" }, "name": { - "type": "String" + "type": "string" }, "display": { - "type": "Boolean" + "type": "boolean" }, "icon": { - "type": "String" + "type": "string" } }, "relations": { diff --git a/modules/item/back/models/item-family.json b/modules/item/back/models/item-family.json new file mode 100644 index 000000000..270c86061 --- /dev/null +++ b/modules/item/back/models/item-family.json @@ -0,0 +1,27 @@ +{ + "name": "ItemFamily", + "base": "VnModel", + "options": { + "mysql": { + "table": "itemFamily" + } + }, + "properties": { + "code": { + "type": "string", + "id": true, + "description": "Identifier" + }, + "description": { + "type": "string" + } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} \ No newline at end of file diff --git a/modules/item/back/models/item-type.json b/modules/item/back/models/item-type.json index b9f038f84..cb9d5ace8 100644 --- a/modules/item/back/models/item-type.json +++ b/modules/item/back/models/item-type.json @@ -8,21 +8,21 @@ }, "properties": { "id": { - "type": "Number", + "type": "number", "id": true, "description": "Identifier" }, "code": { - "type": "String" + "type": "string" }, "name": { - "type": "String" + "type": "string" }, "life": { - "type": "Number" + "type": "number" }, "isPackaging": { - "type": "Boolean" + "type": "boolean" } }, "relations": { diff --git a/modules/item/back/models/origin.json b/modules/item/back/models/origin.json index c381600bf..d2fe3fdf0 100644 --- a/modules/item/back/models/origin.json +++ b/modules/item/back/models/origin.json @@ -8,15 +8,15 @@ }, "properties": { "id": { - "type": "Number", + "type": "number", "id": true, "description": "Identifier" }, "code": { - "type": "String" + "type": "string" }, "name": { - "type": "String" + "type": "string" } }, "acls": [ diff --git a/modules/item/front/fixed-price-search-panel/index.html b/modules/item/front/fixed-price-search-panel/index.html index e34c55ccf..5c8a58674 100644 --- a/modules/item/front/fixed-price-search-panel/index.html +++ b/modules/item/front/fixed-price-search-panel/index.html @@ -44,7 +44,7 @@ + on-change="$ctrl.upsertPrice(price)" + step="0.01"> @@ -140,7 +141,7 @@ + ng-click="deleteFixedPrice.show({$index})"> @@ -154,9 +155,19 @@ ng-click="model.insert()">
+ +
- \ No newline at end of file + + + \ No newline at end of file diff --git a/modules/item/front/fixed-price/locale/es.yml b/modules/item/front/fixed-price/locale/es.yml index c19b7703c..f52aef02c 100644 --- a/modules/item/front/fixed-price/locale/es.yml +++ b/modules/item/front/fixed-price/locale/es.yml @@ -1,4 +1,5 @@ Fixed prices: Precios fijados Search prices by item ID or code: Buscar por ID de artículo o código Search fixed prices: Buscar precios fijados -Add fixed price: Añadir precio fijado \ No newline at end of file +Add fixed price: Añadir precio fijado +This row will be removed: Esta linea se eliminará \ No newline at end of file diff --git a/modules/item/front/index/index.html b/modules/item/front/index/index.html index 023295042..816777a74 100644 --- a/modules/item/front/index/index.html +++ b/modules/item/front/index/index.html @@ -1,115 +1,148 @@ - - - - - - - Id - Grouping - Packing - Description - Stems - Size - Type - Category - Intrastat - Origin - Buyer - Density - Multiplier - Active - Landed - - - - - - - - - - - {{::item.id}} - - - {{::item.grouping | dashIfEmpty}} - {{::item.packing | dashIfEmpty}} - -
- {{::item.name}} - -

{{::item.subName}}

-
-
- - -
- {{::item.stems}} - {{::item.size}} - - {{::item.typeName}} - - - {{::item.category}} - - - {{::item.intrastat}} - - {{::item.origin}} - - - {{::item.userName}} - - - {{::item.density}} - {{::item.stemMultiplier}} - - - - - {{::item.landed | date:'dd/MM/yyyy'}} - - - - - - - - -
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Identifier + + Grouping + + Packing + + Description + + Stems + + Size + + Type + + Category + + Intrastat + + Origin + + Buyer + + Density + + Multiplier + + Active + + Landed +
+ + + + {{::item.id}} + + {{::item.grouping | dashIfEmpty}}{{::item.packing | dashIfEmpty}} +
+ {{::item.name}} + +

{{::item.subName}}

+
+
+ + +
{{::item.stems}}{{::item.size}} + {{::item.typeName}} + + {{::item.category}} + + {{::item.intrastat}} + {{::item.origin}} + + {{::item.userName}} + + {{::item.density}}{{::item.stemMultiplier}} + + + {{::item.landed | date:'dd/MM/yyyy'}} + + + + + + +
+
+
+
@@ -133,7 +166,7 @@ diff --git a/modules/item/front/index/index.js b/modules/item/front/index/index.js index 3235d684e..915027c3c 100644 --- a/modules/item/front/index/index.js +++ b/modules/item/front/index/index.js @@ -5,9 +5,61 @@ import './style.scss'; class Controller extends Section { constructor($element, $) { super($element, $); - this.showFields = { - id: false, - actions: false + + 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 + }, + ] }; } @@ -15,7 +67,7 @@ class Controller extends Section { switch (param) { case 'category': return {'ic.name': value}; - case 'salesPersonFk': + case 'buyerFk': return {'it.workerFk': value}; case 'grouping': return {'b.grouping': value}; @@ -27,9 +79,10 @@ class Controller extends Section { return {'i.typeFk': value}; case 'intrastat': return {'intr.description': value}; + case 'name': + return {'i.name': {like: `%${value}%`}}; case 'id': case 'size': - case 'name': case 'subname': case 'isActive': case 'density': diff --git a/modules/item/front/index/style.scss b/modules/item/front/index/style.scss index b0b94c19d..eaa1a16ed 100644 --- a/modules/item/front/index/style.scss +++ b/modules/item/front/index/style.scss @@ -23,7 +23,7 @@ vn-item-product { } } -vn-table { +table { img { border-radius: 50%; width: 50px; diff --git a/modules/item/front/last-entries/index.html b/modules/item/front/last-entries/index.html index af7bbd751..29047c613 100644 --- a/modules/item/front/last-entries/index.html +++ b/modules/item/front/last-entries/index.html @@ -23,7 +23,6 @@ - diff --git a/modules/monitor/front/index/locale/es.yml b/modules/monitor/front/index/locale/es.yml index b17861e9f..3a115797d 100644 --- a/modules/monitor/front/index/locale/es.yml +++ b/modules/monitor/front/index/locale/es.yml @@ -9,5 +9,6 @@ Minimize/Maximize: Minimizar/Maximizar Problems: Problemas Theoretical: Teórica Practical: Práctica +Preparation: Preparación Auto-refresh: Auto-refresco Toggle auto-refresh every 2 minutes: Conmuta el refresco automático cada 2 minutos \ No newline at end of file diff --git a/modules/monitor/front/index/tickets/index.html b/modules/monitor/front/index/tickets/index.html index 04f7f339f..34f2841fd 100644 --- a/modules/monitor/front/index/tickets/index.html +++ b/modules/monitor/front/index/tickets/index.html @@ -20,155 +20,187 @@ Tickets monitor - - + + + - - - - - - - - - - Problems - Client - Salesperson - Date - Prep. - Theoretical - Practical - Province - State - Zone - Total - - - - - - - - - - - - - - - - - - - - - - {{::ticket.nickname}} - - - - - {{::ticket.userName | dashIfEmpty}} - - - - - {{::ticket.shipped | date: 'dd/MM/yyyy'}} - - - {{::ticket.shipped | date: 'HH:mm'}} - {{::ticket.zoneLanding | date: 'HH:mm'}} - {{::ticket.practicalHour | date: 'HH:mm'}} - {{::ticket.province}} - - - {{::ticket.refFk}} - - - {{::ticket.state}} - - - - - {{::ticket.zoneName | dashIfEmpty}} - - - - - {{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}} - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + }"> + + + + + + + + + + + + + + + +
+ Problems + + Identifier + + Client + + Salesperson + + Date + + Theoretical + + Practical + + Preparation + + Province + + State + + Zone + + Total +
+ + + + + + + + + + + + + + + {{::ticket.id}} + + + + {{::ticket.nickname}} + + + + {{::ticket.userName | dashIfEmpty}} + + + + {{::ticket.shipped | date: 'dd/MM/yyyy'}} + + {{::ticket.zoneLanding | date: 'HH:mm'}}{{::ticket.practicalHour | date: 'HH:mm'}}{{::ticket.shipped | date: 'HH:mm'}}{{::ticket.province}} + + {{::ticket.refFk}} + + + {{::ticket.state}} + + + + {{::ticket.zoneName | dashIfEmpty}} + + + + {{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}} + + + + + + +
+
+
@@ -185,7 +217,7 @@ model="model"> - { + const attachments = []; + for (let attachment of options.attachments) { + const fileName = attachment.filename; + const filePath = attachment.path; + // if (fileName.includes('.png')) return; + + if (fileName || filePath) + attachments.push(filePath ? filePath : fileName); + } + const fileNames = attachments.join(',\n'); await db.rawSql(` - INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, status) - VALUES (?, ?, 1, ?, ?, ?)`, [ + INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) + VALUES (?, ?, 1, ?, ?, ?, ?)`, [ options.to, options.replyTo, options.subject, options.text || options.html, + fileNames, error && error.message || 'Sent' ]); });