diff --git a/.circleci/config.yml b/.circleci/config.yml index 79a9f7217..4233aced8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -146,7 +146,7 @@ jobs: test-rome-hasura-migrate: docker: - - image: cimg/node:16.15.0 + - image: cimg/node:18.18.2 parameters: secret: type: string @@ -178,15 +178,19 @@ jobs: - run: npm run build:test:rome + - aws-cli/setup: + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: AWS_REGION + - aws-s3/sync: - from: build + from: dist to: "s3://rome-online-test/" arguments: "--exclude '*.map'" - test-hasura-migrate: docker: - - image: cimg/node:16.15.0 + - image: cimg/node:18.18.2 parameters: secret: type: string @@ -245,7 +249,7 @@ jobs: region: AWS_REGION - aws-s3/sync: - from: build + from: dist to: "s3://imex-online-test-beta/" arguments: "--exclude '*.map'" @@ -325,16 +329,16 @@ workflows: secret: ${HASURA_TEST_SECRET} filters: branches: - only: test + only: test-AIO - test-rome-app-build: filters: branches: - only: rome/test + only: test-AIO - test-rome-hasura-migrate: secret: ${HASURA_ROME_TEST_SECRET} filters: branches: - only: rome/test + only: test-AIO #- admin-app-build: #filters: #branches: diff --git a/.prettierrc.js b/.prettierrc.js index 4f012c6d9..f2c6fc849 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,16 +1,18 @@ -exports.default = { +const config = { printWidth: 120, useTabs: false, tabWidth: 2, - trailingComma: 'es5', + trailingComma: "none", semi: true, singleQuote: false, bracketSpacing: true, - arrowParens: 'always', + arrowParens: "always", jsxSingleQuote: false, bracketSameLine: false, - endOfLine: 'lf', - importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'], + endOfLine: "lf", + importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], importOrderSeparation: true, - importOrderSortSpecifiers: true, + importOrderSortSpecifiers: true }; + +module.exports = config; diff --git a/_reference/Test_CDK_Acct Config.json b/_reference/Test_CDK_Acct Config.json index 44a140906..d8e7da4fc 100644 --- a/_reference/Test_CDK_Acct Config.json +++ b/_reference/Test_CDK_Acct Config.json @@ -567,4 +567,4 @@ "description": "Exempt" } ] -} \ No newline at end of file +} diff --git a/_reference/test-ecoystem.config.js b/_reference/test-ecoystem.config.js index 37bae797c..1bf66e16c 100644 --- a/_reference/test-ecoystem.config.js +++ b/_reference/test-ecoystem.config.js @@ -1,20 +1,20 @@ module.exports = { - apps: [ - { - name: "IO Test API", - cwd: "./io", - script: "./server.js", - env: { - NODE_ENV: "test", - }, - }, + apps: [ + { + name: "IO Test API", + cwd: "./io", + script: "./server.js", + env: { + NODE_ENV: "test" + } + }, - { - name: "Bitbucket Webhook", - script: "./webhook/index.js", - env: { - NODE_ENV: "production", - }, - }, - ], + { + name: "Bitbucket Webhook", + script: "./webhook/index.js", + env: { + NODE_ENV: "production" + } + } + ] }; diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 105fb3b01..cbc7e04be 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -18897,6 +18897,27 @@ + + media + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + message false diff --git a/client/craco.config.js b/client/craco.config.js index 54f46cdb6..87abf5bcf 100644 --- a/client/craco.config.js +++ b/client/craco.config.js @@ -1,10 +1,10 @@ // craco.config.js const TerserPlugin = require("terser-webpack-plugin"); const CracoLessPlugin = require("craco-less"); -const {convertLegacyToken} = require('@ant-design/compatible/lib'); -const {theme} = require('antd/lib'); +const { convertLegacyToken } = require("@ant-design/compatible/lib"); +const { theme } = require("antd/lib"); -const {defaultAlgorithm, defaultSeed} = theme; +const { defaultAlgorithm, defaultSeed } = theme; const mapToken = defaultAlgorithm(defaultSeed); const v4Token = convertLegacyToken(mapToken); @@ -12,43 +12,42 @@ const v4Token = convertLegacyToken(mapToken); // TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely. module.exports = { - plugins: [ - - { - plugin: CracoLessPlugin, - options: { - lessLoaderOptions: { - lessOptions: { - modifyVars: {...v4Token}, - javascriptEnabled: true, - }, - }, - }, + plugins: [ + { + plugin: CracoLessPlugin, + options: { + lessLoaderOptions: { + lessOptions: { + modifyVars: { ...v4Token }, + javascriptEnabled: true + } + } + } + } + ], + webpack: { + configure: (webpackConfig) => { + return { + ...webpackConfig, + // Required for Dev Server + devServer: { + ...webpackConfig.devServer, + allowedHosts: "all" }, - ], - webpack: { - configure: (webpackConfig) => { - return { - ...webpackConfig, - // Required for Dev Server - devServer: { - ...webpackConfig.devServer, - allowedHosts: 'all', - }, - optimization: { - ...webpackConfig.optimization, - // Workaround for CircleCI bug caused by the number of CPUs shown - // https://github.com/facebook/create-react-app/issues/8320 - minimizer: webpackConfig.optimization.minimizer.map((item) => { - if (item instanceof TerserPlugin) { - item.options.parallel = 2; - } + optimization: { + ...webpackConfig.optimization, + // Workaround for CircleCI bug caused by the number of CPUs shown + // https://github.com/facebook/create-react-app/issues/8320 + minimizer: webpackConfig.optimization.minimizer.map((item) => { + if (item instanceof TerserPlugin) { + item.options.parallel = 2; + } - return item; - }), - }, - }; - }, - }, - devtool: "source-map", + return item; + }) + } + }; + } + }, + devtool: "source-map" }; diff --git a/client/cypress.config.js b/client/cypress.config.js index f227b0b40..1f2d28ce6 100644 --- a/client/cypress.config.js +++ b/client/cypress.config.js @@ -1,17 +1,17 @@ -const {defineConfig} = require('cypress') +const { defineConfig } = require("cypress"); module.exports = defineConfig({ - experimentalStudio: true, - env: { - FIREBASE_USERNAME: 'cypress@imex.test', - FIREBASE_PASSWORD: 'cypress', + experimentalStudio: true, + env: { + FIREBASE_USERNAME: "cypress@imex.test", + FIREBASE_PASSWORD: "cypress" + }, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require("./cypress/plugins/index.js")(on, config); }, - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'http://localhost:3000', - }, -}) + baseUrl: "http://localhost:3000" + } +}); diff --git a/client/cypress/e2e/01-General Render/01-home.cy.js b/client/cypress/e2e/01-General Render/01-home.cy.js index 120baa84f..44202154f 100644 --- a/client/cypress/e2e/01-General Render/01-home.cy.js +++ b/client/cypress/e2e/01-General Render/01-home.cy.js @@ -1,24 +1,19 @@ /// -const {FIREBASE_USERNAME, FIREBASE_PASSWORcD} = Cypress.env(); +const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env(); describe("Renders the General Page", () => { - beforeEach(() => { - cy.visit("/"); - }); - it("Renders Correctly", () => { - }); - it("Has the Slogan", () => { - cy.findByText("A whole x22new kind of shop management system.").should( - "exist" - ); - /* ==== Generated with Cypress Studio ==== */ - cy.get( - ".ant-menu-item-active > .ant-menu-title-content > .header0-item-block" - ).click(); - cy.get("#email").clear(); - cy.get("#email").type("patrick@imex.dev"); - cy.get("#password").clear(); - cy.get("#password").type("patrick123{enter}"); - cy.get(".ant-form > .ant-btn").click(); - /* ==== End Cypress Studio ==== */ - }); + beforeEach(() => { + cy.visit("/"); + }); + it("Renders Correctly", () => {}); + it("Has the Slogan", () => { + cy.findByText("A whole x22new kind of shop management system.").should("exist"); + /* ==== Generated with Cypress Studio ==== */ + cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click(); + cy.get("#email").clear(); + cy.get("#email").type("patrick@imex.dev"); + cy.get("#password").clear(); + cy.get("#password").type("patrick123{enter}"); + cy.get(".ant-form > .ant-btn").click(); + /* ==== End Cypress Studio ==== */ + }); }); diff --git a/client/cypress/e2e/1-getting-started/todo.cy.js b/client/cypress/e2e/1-getting-started/todo.cy.js index 28c0f35e1..87e609ced 100644 --- a/client/cypress/e2e/1-getting-started/todo.cy.js +++ b/client/cypress/e2e/1-getting-started/todo.cy.js @@ -11,133 +11,114 @@ // please read our getting started guide: // https://on.cypress.io/introduction-to-cypress -describe('example to-do app', () => { +describe("example to-do app", () => { + beforeEach(() => { + // Cypress starts out with a blank slate for each test + // so we must tell it to visit our website with the `cy.visit()` command. + // Since we want to visit the same URL at the start of all our tests, + // we include it in our beforeEach function so that it runs before each test + cy.visit("https://example.cypress.io/todo"); + }); + + it("displays two todo items by default", () => { + // We use the `cy.get()` command to get all elements that match the selector. + // Then, we use `should` to assert that there are two matched items, + // which are the two default items. + cy.get(".todo-list li").should("have.length", 2); + + // We can go even further and check that the default todos each contain + // the correct text. We use the `first` and `last` functions + // to get just the first and last matched elements individually, + // and then perform an assertion with `should`. + cy.get(".todo-list li").first().should("have.text", "Pay electric bill"); + cy.get(".todo-list li").last().should("have.text", "Walk the dog"); + }); + + it("can add new todo items", () => { + // We'll store our item text in a variable so we can reuse it + const newItem = "Feed the cat"; + + // Let's get the input element and use the `type` command to + // input our new list item. After typing the content of our item, + // we need to type the enter key as well in order to submit the input. + // This input has a data-test attribute so we'll use that to select the + // element in accordance with best practices: + // https://on.cypress.io/selecting-elements + cy.get("[data-test=new-todo]").type(`${newItem}{enter}`); + + // Now that we've typed our new item, let's check that it actually was added to the list. + // Since it's the newest item, it should exist as the last element in the list. + // In addition, with the two default items, we should have a total of 3 elements in the list. + // Since assertions yield the element that was asserted on, + // we can chain both of these assertions together into a single statement. + cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem); + }); + + it("can check off an item as completed", () => { + // In addition to using the `get` command to get an element by selector, + // we can also use the `contains` command to get an element by its contents. + // However, this will yield the , which is lowest-level element that contains the text. + // In order to check the item, we'll find the element for this + // by traversing up the dom to the parent element. From there, we can `find` + // the child checkbox element and use the `check` command to check it. + cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check(); + + // Now that we've checked the button, we can go ahead and make sure + // that the list element is now marked as completed. + // Again we'll use `contains` to find the element and then use the `parents` command + // to traverse multiple levels up the dom until we find the corresponding element. + // Once we get that element, we can assert that it has the completed class. + cy.contains("Pay electric bill").parents("li").should("have.class", "completed"); + }); + + context("with a checked task", () => { beforeEach(() => { - // Cypress starts out with a blank slate for each test - // so we must tell it to visit our website with the `cy.visit()` command. - // Since we want to visit the same URL at the start of all our tests, - // we include it in our beforeEach function so that it runs before each test - cy.visit('https://example.cypress.io/todo') - }) + // We'll take the command we used above to check off an element + // Since we want to perform multiple tests that start with checking + // one element, we put it in the beforeEach hook + // so that it runs at the start of every test. + cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check(); + }); - it('displays two todo items by default', () => { - // We use the `cy.get()` command to get all elements that match the selector. - // Then, we use `should` to assert that there are two matched items, - // which are the two default items. - cy.get('.todo-list li').should('have.length', 2) + it("can filter for uncompleted tasks", () => { + // We'll click on the "active" button in order to + // display only incomplete items + cy.contains("Active").click(); - // We can go even further and check that the default todos each contain - // the correct text. We use the `first` and `last` functions - // to get just the first and last matched elements individually, - // and then perform an assertion with `should`. - cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') - cy.get('.todo-list li').last().should('have.text', 'Walk the dog') - }) + // After filtering, we can assert that there is only the one + // incomplete item in the list. + cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog"); - it('can add new todo items', () => { - // We'll store our item text in a variable so we can reuse it - const newItem = 'Feed the cat' + // For good measure, let's also assert that the task we checked off + // does not exist on the page. + cy.contains("Pay electric bill").should("not.exist"); + }); - // Let's get the input element and use the `type` command to - // input our new list item. After typing the content of our item, - // we need to type the enter key as well in order to submit the input. - // This input has a data-test attribute so we'll use that to select the - // element in accordance with best practices: - // https://on.cypress.io/selecting-elements - cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) + it("can filter for completed tasks", () => { + // We can perform similar steps as the test above to ensure + // that only completed tasks are shown + cy.contains("Completed").click(); - // Now that we've typed our new item, let's check that it actually was added to the list. - // Since it's the newest item, it should exist as the last element in the list. - // In addition, with the two default items, we should have a total of 3 elements in the list. - // Since assertions yield the element that was asserted on, - // we can chain both of these assertions together into a single statement. - cy.get('.todo-list li') - .should('have.length', 3) - .last() - .should('have.text', newItem) - }) + cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill"); - it('can check off an item as completed', () => { - // In addition to using the `get` command to get an element by selector, - // we can also use the `contains` command to get an element by its contents. - // However, this will yield the , which is lowest-level element that contains the text. - // In order to check the item, we'll find the element for this - // by traversing up the dom to the parent element. From there, we can `find` - // the child checkbox element and use the `check` command to check it. - cy.contains('Pay electric bill') - .parent() - .find('input[type=checkbox]') - .check() + cy.contains("Walk the dog").should("not.exist"); + }); - // Now that we've checked the button, we can go ahead and make sure - // that the list element is now marked as completed. - // Again we'll use `contains` to find the element and then use the `parents` command - // to traverse multiple levels up the dom until we find the corresponding element. - // Once we get that element, we can assert that it has the completed class. - cy.contains('Pay electric bill') - .parents('li') - .should('have.class', 'completed') - }) + it("can delete all completed tasks", () => { + // First, let's click the "Clear completed" button + // `contains` is actually serving two purposes here. + // First, it's ensuring that the button exists within the dom. + // This button only appears when at least one task is checked + // so this command is implicitly verifying that it does exist. + // Second, it selects the button so we can click it. + cy.contains("Clear completed").click(); - context('with a checked task', () => { - beforeEach(() => { - // We'll take the command we used above to check off an element - // Since we want to perform multiple tests that start with checking - // one element, we put it in the beforeEach hook - // so that it runs at the start of every test. - cy.contains('Pay electric bill') - .parent() - .find('input[type=checkbox]') - .check() - }) + // Then we can make sure that there is only one element + // in the list and our element does not exist + cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill"); - it('can filter for uncompleted tasks', () => { - // We'll click on the "active" button in order to - // display only incomplete items - cy.contains('Active').click() - - // After filtering, we can assert that there is only the one - // incomplete item in the list. - cy.get('.todo-list li') - .should('have.length', 1) - .first() - .should('have.text', 'Walk the dog') - - // For good measure, let's also assert that the task we checked off - // does not exist on the page. - cy.contains('Pay electric bill').should('not.exist') - }) - - it('can filter for completed tasks', () => { - // We can perform similar steps as the test above to ensure - // that only completed tasks are shown - cy.contains('Completed').click() - - cy.get('.todo-list li') - .should('have.length', 1) - .first() - .should('have.text', 'Pay electric bill') - - cy.contains('Walk the dog').should('not.exist') - }) - - it('can delete all completed tasks', () => { - // First, let's click the "Clear completed" button - // `contains` is actually serving two purposes here. - // First, it's ensuring that the button exists within the dom. - // This button only appears when at least one task is checked - // so this command is implicitly verifying that it does exist. - // Second, it selects the button so we can click it. - cy.contains('Clear completed').click() - - // Then we can make sure that there is only one element - // in the list and our element does not exist - cy.get('.todo-list li') - .should('have.length', 1) - .should('not.have.text', 'Pay electric bill') - - // Finally, make sure that the clear button no longer exists. - cy.contains('Clear completed').should('not.exist') - }) - }) -}) + // Finally, make sure that the clear button no longer exists. + cy.contains("Clear completed").should("not.exist"); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/actions.cy.js b/client/cypress/e2e/2-advanced-examples/actions.cy.js index f29212a95..1663fba51 100644 --- a/client/cypress/e2e/2-advanced-examples/actions.cy.js +++ b/client/cypress/e2e/2-advanced-examples/actions.cy.js @@ -1,299 +1,284 @@ /// -context('Actions', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/actions') - }) +context("Actions", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/actions"); + }); - // https://on.cypress.io/interacting-with-elements + // https://on.cypress.io/interacting-with-elements - it('.type() - type into a DOM element', () => { - // https://on.cypress.io/type - cy.get('.action-email') - .type('fake@email.com').should('have.value', 'fake@email.com') + it(".type() - type into a DOM element", () => { + // https://on.cypress.io/type + cy.get(".action-email") + .type("fake@email.com") + .should("have.value", "fake@email.com") - // .type() with special character sequences - .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') - .type('{del}{selectall}{backspace}') + // .type() with special character sequences + .type("{leftarrow}{rightarrow}{uparrow}{downarrow}") + .type("{del}{selectall}{backspace}") - // .type() with key modifiers - .type('{alt}{option}') //these are equivalent - .type('{ctrl}{control}') //these are equivalent - .type('{meta}{command}{cmd}') //these are equivalent - .type('{shift}') + // .type() with key modifiers + .type("{alt}{option}") //these are equivalent + .type("{ctrl}{control}") //these are equivalent + .type("{meta}{command}{cmd}") //these are equivalent + .type("{shift}") - // Delay each keypress by 0.1 sec - .type('slow.typing@email.com', {delay: 100}) - .should('have.value', 'slow.typing@email.com') + // Delay each keypress by 0.1 sec + .type("slow.typing@email.com", { delay: 100 }) + .should("have.value", "slow.typing@email.com"); - cy.get('.action-disabled') - // Ignore error checking prior to type - // like whether the input is visible or disabled - .type('disabled error checking', {force: true}) - .should('have.value', 'disabled error checking') - }) + cy.get(".action-disabled") + // Ignore error checking prior to type + // like whether the input is visible or disabled + .type("disabled error checking", { force: true }) + .should("have.value", "disabled error checking"); + }); - it('.focus() - focus on a DOM element', () => { - // https://on.cypress.io/focus - cy.get('.action-focus').focus() - .should('have.class', 'focus') - .prev().should('have.attr', 'style', 'color: orange;') - }) + it(".focus() - focus on a DOM element", () => { + // https://on.cypress.io/focus + cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;"); + }); - it('.blur() - blur off a DOM element', () => { - // https://on.cypress.io/blur - cy.get('.action-blur').type('About to blur').blur() - .should('have.class', 'error') - .prev().should('have.attr', 'style', 'color: red;') - }) + it(".blur() - blur off a DOM element", () => { + // https://on.cypress.io/blur + cy.get(".action-blur") + .type("About to blur") + .blur() + .should("have.class", "error") + .prev() + .should("have.attr", "style", "color: red;"); + }); - it('.clear() - clears an input or textarea element', () => { - // https://on.cypress.io/clear - cy.get('.action-clear').type('Clear this text') - .should('have.value', 'Clear this text') - .clear() - .should('have.value', '') - }) + it(".clear() - clears an input or textarea element", () => { + // https://on.cypress.io/clear + cy.get(".action-clear") + .type("Clear this text") + .should("have.value", "Clear this text") + .clear() + .should("have.value", ""); + }); - it('.submit() - submit a form', () => { - // https://on.cypress.io/submit - cy.get('.action-form') - .find('[type="text"]').type('HALFOFF') + it(".submit() - submit a form", () => { + // https://on.cypress.io/submit + cy.get(".action-form").find('[type="text"]').type("HALFOFF"); - cy.get('.action-form').submit() - .next().should('contain', 'Your form has been submitted!') - }) + cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!"); + }); - it('.click() - click on a DOM element', () => { - // https://on.cypress.io/click - cy.get('.action-btn').click() + it(".click() - click on a DOM element", () => { + // https://on.cypress.io/click + cy.get(".action-btn").click(); - // You can click on 9 specific positions of an element: - // ----------------------------------- - // | topLeft top topRight | - // | | - // | | - // | | - // | left center right | - // | | - // | | - // | | - // | bottomLeft bottom bottomRight | - // ----------------------------------- + // You can click on 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- - // clicking in the center of the element is the default - cy.get('#action-canvas').click() + // clicking in the center of the element is the default + cy.get("#action-canvas").click(); - cy.get('#action-canvas').click('topLeft') - cy.get('#action-canvas').click('top') - cy.get('#action-canvas').click('topRight') - cy.get('#action-canvas').click('left') - cy.get('#action-canvas').click('right') - cy.get('#action-canvas').click('bottomLeft') - cy.get('#action-canvas').click('bottom') - cy.get('#action-canvas').click('bottomRight') + cy.get("#action-canvas").click("topLeft"); + cy.get("#action-canvas").click("top"); + cy.get("#action-canvas").click("topRight"); + cy.get("#action-canvas").click("left"); + cy.get("#action-canvas").click("right"); + cy.get("#action-canvas").click("bottomLeft"); + cy.get("#action-canvas").click("bottom"); + cy.get("#action-canvas").click("bottomRight"); - // .click() accepts an x and y coordinate - // that controls where the click occurs :) + // .click() accepts an x and y coordinate + // that controls where the click occurs :) - cy.get('#action-canvas') - .click(80, 75) // click 80px on x coord and 75px on y coord - .click(170, 75) - .click(80, 165) - .click(100, 185) - .click(125, 190) - .click(150, 185) - .click(170, 165) + cy.get("#action-canvas") + .click(80, 75) // click 80px on x coord and 75px on y coord + .click(170, 75) + .click(80, 165) + .click(100, 185) + .click(125, 190) + .click(150, 185) + .click(170, 165); - // click multiple elements by passing multiple: true - cy.get('.action-labels>.label').click({multiple: true}) + // click multiple elements by passing multiple: true + cy.get(".action-labels>.label").click({ multiple: true }); - // Ignore error checking prior to clicking - cy.get('.action-opacity>.btn').click({force: true}) - }) + // Ignore error checking prior to clicking + cy.get(".action-opacity>.btn").click({ force: true }); + }); - it('.dblclick() - double click on a DOM element', () => { - // https://on.cypress.io/dblclick + it(".dblclick() - double click on a DOM element", () => { + // https://on.cypress.io/dblclick - // Our app has a listener on 'dblclick' event in our 'scripts.js' - // that hides the div and shows an input on double click - cy.get('.action-div').dblclick().should('not.be.visible') - cy.get('.action-input-hidden').should('be.visible') - }) + // Our app has a listener on 'dblclick' event in our 'scripts.js' + // that hides the div and shows an input on double click + cy.get(".action-div").dblclick().should("not.be.visible"); + cy.get(".action-input-hidden").should("be.visible"); + }); - it('.rightclick() - right click on a DOM element', () => { - // https://on.cypress.io/rightclick + it(".rightclick() - right click on a DOM element", () => { + // https://on.cypress.io/rightclick - // Our app has a listener on 'contextmenu' event in our 'scripts.js' - // that hides the div and shows an input on right click - cy.get('.rightclick-action-div').rightclick().should('not.be.visible') - cy.get('.rightclick-action-input-hidden').should('be.visible') - }) + // Our app has a listener on 'contextmenu' event in our 'scripts.js' + // that hides the div and shows an input on right click + cy.get(".rightclick-action-div").rightclick().should("not.be.visible"); + cy.get(".rightclick-action-input-hidden").should("be.visible"); + }); - it('.check() - check a checkbox or radio element', () => { - // https://on.cypress.io/check + it(".check() - check a checkbox or radio element", () => { + // https://on.cypress.io/check - // By default, .check() will check all - // matching checkbox or radio elements in succession, one after another - cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') - .check().should('be.checked') + // By default, .check() will check all + // matching checkbox or radio elements in succession, one after another + cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked"); - cy.get('.action-radios [type="radio"]').not('[disabled]') - .check().should('be.checked') + cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked"); - // .check() accepts a value argument - cy.get('.action-radios [type="radio"]') - .check('radio1').should('be.checked') + // .check() accepts a value argument + cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked"); - // .check() accepts an array of values - cy.get('.action-multiple-checkboxes [type="checkbox"]') - .check(['checkbox1', 'checkbox2']).should('be.checked') + // .check() accepts an array of values + cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked"); - // Ignore error checking prior to checking - cy.get('.action-checkboxes [disabled]') - .check({force: true}).should('be.checked') + // Ignore error checking prior to checking + cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked"); - cy.get('.action-radios [type="radio"]') - .check('radio3', {force: true}).should('be.checked') - }) + cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked"); + }); - it('.uncheck() - uncheck a checkbox element', () => { - // https://on.cypress.io/uncheck + it(".uncheck() - uncheck a checkbox element", () => { + // https://on.cypress.io/uncheck - // By default, .uncheck() will uncheck all matching - // checkbox elements in succession, one after another - cy.get('.action-check [type="checkbox"]') - .not('[disabled]') - .uncheck().should('not.be.checked') + // By default, .uncheck() will uncheck all matching + // checkbox elements in succession, one after another + cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked"); - // .uncheck() accepts a value argument - cy.get('.action-check [type="checkbox"]') - .check('checkbox1') - .uncheck('checkbox1').should('not.be.checked') + // .uncheck() accepts a value argument + cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked"); - // .uncheck() accepts an array of values - cy.get('.action-check [type="checkbox"]') - .check(['checkbox1', 'checkbox3']) - .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') + // .uncheck() accepts an array of values + cy.get('.action-check [type="checkbox"]') + .check(["checkbox1", "checkbox3"]) + .uncheck(["checkbox1", "checkbox3"]) + .should("not.be.checked"); - // Ignore error checking prior to unchecking - cy.get('.action-check [disabled]') - .uncheck({force: true}).should('not.be.checked') - }) + // Ignore error checking prior to unchecking + cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked"); + }); - it('.select() - select an option in a element', () => { - // https://on.cypress.io/select + it(".select() - select an option in a element", () => { + // https://on.cypress.io/select - // at first, no option should be selected - cy.get('.action-select') - .should('have.value', '--Select a fruit--') + // at first, no option should be selected + cy.get(".action-select").should("have.value", "--Select a fruit--"); - // Select option(s) with matching text content - cy.get('.action-select').select('apples') - // confirm the apples were selected - // note that each value starts with "fr-" in our HTML - cy.get('.action-select').should('have.value', 'fr-apples') + // Select option(s) with matching text content + cy.get(".action-select").select("apples"); + // confirm the apples were selected + // note that each value starts with "fr-" in our HTML + cy.get(".action-select").should("have.value", "fr-apples"); - cy.get('.action-select-multiple') - .select(['apples', 'oranges', 'bananas']) - // when getting multiple values, invoke "val" method first - .invoke('val') - .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) + cy.get(".action-select-multiple") + .select(["apples", "oranges", "bananas"]) + // when getting multiple values, invoke "val" method first + .invoke("val") + .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]); - // Select option(s) with matching value - cy.get('.action-select').select('fr-bananas') - // can attach an assertion right away to the element - .should('have.value', 'fr-bananas') + // Select option(s) with matching value + cy.get(".action-select") + .select("fr-bananas") + // can attach an assertion right away to the element + .should("have.value", "fr-bananas"); - cy.get('.action-select-multiple') - .select(['fr-apples', 'fr-oranges', 'fr-bananas']) - .invoke('val') - .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) + cy.get(".action-select-multiple") + .select(["fr-apples", "fr-oranges", "fr-bananas"]) + .invoke("val") + .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]); - // assert the selected values include oranges - cy.get('.action-select-multiple') - .invoke('val').should('include', 'fr-oranges') - }) + // assert the selected values include oranges + cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges"); + }); - it('.scrollIntoView() - scroll an element into view', () => { - // https://on.cypress.io/scrollintoview + it(".scrollIntoView() - scroll an element into view", () => { + // https://on.cypress.io/scrollintoview - // normally all of these buttons are hidden, - // because they're not within - // the viewable area of their parent - // (we need to scroll to see them) - cy.get('#scroll-horizontal button') - .should('not.be.visible') + // normally all of these buttons are hidden, + // because they're not within + // the viewable area of their parent + // (we need to scroll to see them) + cy.get("#scroll-horizontal button").should("not.be.visible"); - // scroll the button into view, as if the user had scrolled - cy.get('#scroll-horizontal button').scrollIntoView() - .should('be.visible') + // scroll the button into view, as if the user had scrolled + cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible"); - cy.get('#scroll-vertical button') - .should('not.be.visible') + cy.get("#scroll-vertical button").should("not.be.visible"); - // Cypress handles the scroll direction needed - cy.get('#scroll-vertical button').scrollIntoView() - .should('be.visible') + // Cypress handles the scroll direction needed + cy.get("#scroll-vertical button").scrollIntoView().should("be.visible"); - cy.get('#scroll-both button') - .should('not.be.visible') + cy.get("#scroll-both button").should("not.be.visible"); - // Cypress knows to scroll to the right and down - cy.get('#scroll-both button').scrollIntoView() - .should('be.visible') - }) + // Cypress knows to scroll to the right and down + cy.get("#scroll-both button").scrollIntoView().should("be.visible"); + }); - it('.trigger() - trigger an event on a DOM element', () => { - // https://on.cypress.io/trigger + it(".trigger() - trigger an event on a DOM element", () => { + // https://on.cypress.io/trigger - // To interact with a range input (slider) - // we need to set its value & trigger the - // event to signal it changed + // To interact with a range input (slider) + // we need to set its value & trigger the + // event to signal it changed - // Here, we invoke jQuery's val() method to set - // the value and trigger the 'change' event - cy.get('.trigger-input-range') - .invoke('val', 25) - .trigger('change') - .get('input[type=range]').siblings('p') - .should('have.text', '25') - }) + // Here, we invoke jQuery's val() method to set + // the value and trigger the 'change' event + cy.get(".trigger-input-range") + .invoke("val", 25) + .trigger("change") + .get("input[type=range]") + .siblings("p") + .should("have.text", "25"); + }); - it('cy.scrollTo() - scroll the window or element to a position', () => { - // https://on.cypress.io/scrollto + it("cy.scrollTo() - scroll the window or element to a position", () => { + // https://on.cypress.io/scrollto - // You can scroll to 9 specific positions of an element: - // ----------------------------------- - // | topLeft top topRight | - // | | - // | | - // | | - // | left center right | - // | | - // | | - // | | - // | bottomLeft bottom bottomRight | - // ----------------------------------- + // You can scroll to 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- - // if you chain .scrollTo() off of cy, we will - // scroll the entire window - cy.scrollTo('bottom') + // if you chain .scrollTo() off of cy, we will + // scroll the entire window + cy.scrollTo("bottom"); - cy.get('#scrollable-horizontal').scrollTo('right') + cy.get("#scrollable-horizontal").scrollTo("right"); - // or you can scroll to a specific coordinate: - // (x axis, y axis) in pixels - cy.get('#scrollable-vertical').scrollTo(250, 250) + // or you can scroll to a specific coordinate: + // (x axis, y axis) in pixels + cy.get("#scrollable-vertical").scrollTo(250, 250); - // or you can scroll to a specific percentage - // of the (width, height) of the element - cy.get('#scrollable-both').scrollTo('75%', '25%') + // or you can scroll to a specific percentage + // of the (width, height) of the element + cy.get("#scrollable-both").scrollTo("75%", "25%"); - // control the easing of the scroll (default is 'swing') - cy.get('#scrollable-vertical').scrollTo('center', {easing: 'linear'}) + // control the easing of the scroll (default is 'swing') + cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" }); - // control the duration of the scroll (in ms) - cy.get('#scrollable-both').scrollTo('center', {duration: 2000}) - }) -}) + // control the duration of the scroll (in ms) + cy.get("#scrollable-both").scrollTo("center", { duration: 2000 }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/aliasing.cy.js b/client/cypress/e2e/2-advanced-examples/aliasing.cy.js index e56215eb7..152b8ece9 100644 --- a/client/cypress/e2e/2-advanced-examples/aliasing.cy.js +++ b/client/cypress/e2e/2-advanced-examples/aliasing.cy.js @@ -1,39 +1,35 @@ /// -context('Aliasing', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/aliasing') - }) +context("Aliasing", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/aliasing"); + }); - it('.as() - alias a DOM element for later use', () => { - // https://on.cypress.io/as + it(".as() - alias a DOM element for later use", () => { + // https://on.cypress.io/as - // Alias a DOM element for use later - // We don't have to traverse to the element - // later in our code, we reference it with @ + // Alias a DOM element for use later + // We don't have to traverse to the element + // later in our code, we reference it with @ - cy.get('.as-table').find('tbody>tr') - .first().find('td').first() - .find('button').as('firstBtn') + cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn"); - // when we reference the alias, we place an - // @ in front of its name - cy.get('@firstBtn').click() + // when we reference the alias, we place an + // @ in front of its name + cy.get("@firstBtn").click(); - cy.get('@firstBtn') - .should('have.class', 'btn-success') - .and('contain', 'Changed') - }) + cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed"); + }); - it('.as() - alias a route for later use', () => { - // Alias the route to wait for its response - cy.intercept('GET', '**/comments/*').as('getComment') + it(".as() - alias a route for later use", () => { + // Alias the route to wait for its response + cy.intercept("GET", "**/comments/*").as("getComment"); - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.network-btn').click() + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get(".network-btn").click(); - // https://on.cypress.io/wait - cy.wait('@getComment').its('response.statusCode').should('eq', 200) - }) -}) + // https://on.cypress.io/wait + cy.wait("@getComment").its("response.statusCode").should("eq", 200); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/assertions.cy.js b/client/cypress/e2e/2-advanced-examples/assertions.cy.js index 872ae8466..2bde3a9a0 100644 --- a/client/cypress/e2e/2-advanced-examples/assertions.cy.js +++ b/client/cypress/e2e/2-advanced-examples/assertions.cy.js @@ -1,177 +1,173 @@ /// -context('Assertions', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/assertions') - }) +context("Assertions", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/assertions"); + }); - describe('Implicit Assertions', () => { - it('.should() - make an assertion about the current subject', () => { - // https://on.cypress.io/should - cy.get('.assertion-table') - .find('tbody tr:last') - .should('have.class', 'success') - .find('td') - .first() - // checking the text of the element in various ways - .should('have.text', 'Column content') - .should('contain', 'Column content') - .should('have.html', 'Column content') - // chai-jquery uses "is()" to check if element matches selector - .should('match', 'td') - // to match text content against a regular expression - // first need to invoke jQuery method text() - // and then match using regular expression - .invoke('text') - .should('match', /column content/i) + describe("Implicit Assertions", () => { + it(".should() - make an assertion about the current subject", () => { + // https://on.cypress.io/should + cy.get(".assertion-table") + .find("tbody tr:last") + .should("have.class", "success") + .find("td") + .first() + // checking the text of the element in various ways + .should("have.text", "Column content") + .should("contain", "Column content") + .should("have.html", "Column content") + // chai-jquery uses "is()" to check if element matches selector + .should("match", "td") + // to match text content against a regular expression + // first need to invoke jQuery method text() + // and then match using regular expression + .invoke("text") + .should("match", /column content/i); - // a better way to check element's text content against a regular expression - // is to use "cy.contains" - // https://on.cypress.io/contains - cy.get('.assertion-table') - .find('tbody tr:last') - // finds first element with text content matching regular expression - .contains('td', /column content/i) - .should('be.visible') + // a better way to check element's text content against a regular expression + // is to use "cy.contains" + // https://on.cypress.io/contains + cy.get(".assertion-table") + .find("tbody tr:last") + // finds first element with text content matching regular expression + .contains("td", /column content/i) + .should("be.visible"); - // for more information about asserting element's text - // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents + // for more information about asserting element's text + // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents + }); + + it(".and() - chain multiple assertions together", () => { + // https://on.cypress.io/and + cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io"); + }); + }); + + describe("Explicit Assertions", () => { + // https://on.cypress.io/assertions + it("expect - make an assertion about a specified subject", () => { + // We can use Chai's BDD style assertions + expect(true).to.be.true; + const o = { foo: "bar" }; + + expect(o).to.equal(o); + expect(o).to.deep.equal({ foo: "bar" }); + // matching text using regular expression + expect("FooBar").to.match(/bar$/i); + }); + + it("pass your own callback function to should()", () => { + // Pass a function to should that can have any number + // of explicit assertions within it. + // The ".should(cb)" function will be retried + // automatically until it passes all your explicit assertions or times out. + cy.get(".assertions-p") + .find("p") + .should(($p) => { + // https://on.cypress.io/$ + // return an array of texts from all of the p's + // @ts-ignore TS6133 unused variable + const texts = $p.map((i, el) => Cypress.$(el).text()); + + // jquery map returns jquery object + // and .get() convert this to simple array + const paragraphs = texts.get(); + + // array should have length of 3 + expect(paragraphs, "has 3 paragraphs").to.have.length(3); + + // use second argument to expect(...) to provide clear + // message with each assertion + expect(paragraphs, "has expected text in each paragraph").to.deep.eq([ + "Some text from first p", + "More text from second p", + "And even more text from third p" + ]); + }); + }); + + it("finds element by class name regex", () => { + cy.get(".docs-header") + .find("div") + // .should(cb) callback function will be retried + .should(($div) => { + expect($div).to.have.length(1); + + const className = $div[0].className; + + expect(className).to.match(/heading-/); }) + // .then(cb) callback is not retried, + // it either passes or fails + .then(($div) => { + expect($div, "text content").to.have.text("Introduction"); + }); + }); - it('.and() - chain multiple assertions together', () => { - // https://on.cypress.io/and - cy.get('.assertions-link') - .should('have.class', 'active') - .and('have.attr', 'href') - .and('include', 'cypress.io') - }) - }) + it("can throw any error", () => { + cy.get(".docs-header") + .find("div") + .should(($div) => { + if ($div.length !== 1) { + // you can throw your own errors + throw new Error("Did not find 1 element"); + } - describe('Explicit Assertions', () => { - // https://on.cypress.io/assertions - it('expect - make an assertion about a specified subject', () => { - // We can use Chai's BDD style assertions - expect(true).to.be.true - const o = {foo: 'bar'} + const className = $div[0].className; - expect(o).to.equal(o) - expect(o).to.deep.equal({foo: 'bar'}) - // matching text using regular expression - expect('FooBar').to.match(/bar$/i) - }) + if (!className.match(/heading-/)) { + throw new Error(`Could not find class "heading-" in ${className}`); + } + }); + }); - it('pass your own callback function to should()', () => { - // Pass a function to should that can have any number - // of explicit assertions within it. - // The ".should(cb)" function will be retried - // automatically until it passes all your explicit assertions or times out. - cy.get('.assertions-p') - .find('p') - .should(($p) => { - // https://on.cypress.io/$ - // return an array of texts from all of the p's - // @ts-ignore TS6133 unused variable - const texts = $p.map((i, el) => Cypress.$(el).text()) + it("matches unknown text between two elements", () => { + /** + * Text from the first element. + * @type {string} + */ + let text; - // jquery map returns jquery object - // and .get() convert this to simple array - const paragraphs = texts.get() + /** + * Normalizes passed text, + * useful before comparing text with spaces and different capitalization. + * @param {string} s Text to normalize + */ + const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase(); - // array should have length of 3 - expect(paragraphs, 'has 3 paragraphs').to.have.length(3) + cy.get(".two-elements") + .find(".first") + .then(($first) => { + // save text from the first element + text = normalizeText($first.text()); + }); - // use second argument to expect(...) to provide clear - // message with each assertion - expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ - 'Some text from first p', - 'More text from second p', - 'And even more text from third p', - ]) - }) - }) + cy.get(".two-elements") + .find(".second") + .should(($div) => { + // we can massage text before comparing + const secondText = normalizeText($div.text()); - it('finds element by class name regex', () => { - cy.get('.docs-header') - .find('div') - // .should(cb) callback function will be retried - .should(($div) => { - expect($div).to.have.length(1) + expect(secondText, "second text").to.equal(text); + }); + }); - const className = $div[0].className + it("assert - assert shape of an object", () => { + const person = { + name: "Joe", + age: 20 + }; - expect(className).to.match(/heading-/) - }) - // .then(cb) callback is not retried, - // it either passes or fails - .then(($div) => { - expect($div, 'text content').to.have.text('Introduction') - }) - }) + assert.isObject(person, "value is object"); + }); - it('can throw any error', () => { - cy.get('.docs-header') - .find('div') - .should(($div) => { - if ($div.length !== 1) { - // you can throw your own errors - throw new Error('Did not find 1 element') - } + it("retries the should callback until assertions pass", () => { + cy.get("#random-number").should(($div) => { + const n = parseFloat($div.text()); - const className = $div[0].className - - if (!className.match(/heading-/)) { - throw new Error(`Could not find class "heading-" in ${className}`) - } - }) - }) - - it('matches unknown text between two elements', () => { - /** - * Text from the first element. - * @type {string} - */ - let text - - /** - * Normalizes passed text, - * useful before comparing text with spaces and different capitalization. - * @param {string} s Text to normalize - */ - const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() - - cy.get('.two-elements') - .find('.first') - .then(($first) => { - // save text from the first element - text = normalizeText($first.text()) - }) - - cy.get('.two-elements') - .find('.second') - .should(($div) => { - // we can massage text before comparing - const secondText = normalizeText($div.text()) - - expect(secondText, 'second text').to.equal(text) - }) - }) - - it('assert - assert shape of an object', () => { - const person = { - name: 'Joe', - age: 20, - } - - assert.isObject(person, 'value is object') - }) - - it('retries the should callback until assertions pass', () => { - cy.get('#random-number') - .should(($div) => { - const n = parseFloat($div.text()) - - expect(n).to.be.gte(1).and.be.lte(10) - }) - }) - }) -}) + expect(n).to.be.gte(1).and.be.lte(10); + }); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/connectors.cy.js b/client/cypress/e2e/2-advanced-examples/connectors.cy.js index 45909bd9f..3cd60d308 100644 --- a/client/cypress/e2e/2-advanced-examples/connectors.cy.js +++ b/client/cypress/e2e/2-advanced-examples/connectors.cy.js @@ -1,97 +1,96 @@ /// -context('Connectors', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/connectors') - }) +context("Connectors", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/connectors"); + }); - it('.each() - iterate over an array of elements', () => { - // https://on.cypress.io/each - cy.get('.connectors-each-ul>li') - .each(($el, index, $list) => { - console.log($el, index, $list) - }) - }) + it(".each() - iterate over an array of elements", () => { + // https://on.cypress.io/each + cy.get(".connectors-each-ul>li").each(($el, index, $list) => { + console.log($el, index, $list); + }); + }); - it('.its() - get properties on the current subject', () => { - // https://on.cypress.io/its - cy.get('.connectors-its-ul>li') - // calls the 'length' property yielding that value - .its('length') - .should('be.gt', 2) - }) + it(".its() - get properties on the current subject", () => { + // https://on.cypress.io/its + cy.get(".connectors-its-ul>li") + // calls the 'length' property yielding that value + .its("length") + .should("be.gt", 2); + }); - it('.invoke() - invoke a function on the current subject', () => { - // our div is hidden in our script.js - // $('.connectors-div').hide() + it(".invoke() - invoke a function on the current subject", () => { + // our div is hidden in our script.js + // $('.connectors-div').hide() - // https://on.cypress.io/invoke - cy.get('.connectors-div').should('be.hidden') - // call the jquery method 'show' on the 'div.container' - .invoke('show') - .should('be.visible') - }) + // https://on.cypress.io/invoke + cy.get(".connectors-div") + .should("be.hidden") + // call the jquery method 'show' on the 'div.container' + .invoke("show") + .should("be.visible"); + }); - it('.spread() - spread an array as individual args to callback function', () => { - // https://on.cypress.io/spread - const arr = ['foo', 'bar', 'baz'] + it(".spread() - spread an array as individual args to callback function", () => { + // https://on.cypress.io/spread + const arr = ["foo", "bar", "baz"]; - cy.wrap(arr).spread((foo, bar, baz) => { - expect(foo).to.eq('foo') - expect(bar).to.eq('bar') - expect(baz).to.eq('baz') + cy.wrap(arr).spread((foo, bar, baz) => { + expect(foo).to.eq("foo"); + expect(bar).to.eq("bar"); + expect(baz).to.eq("baz"); + }); + }); + + describe(".then()", () => { + it("invokes a callback function with the current subject", () => { + // https://on.cypress.io/then + cy.get(".connectors-list > li").then(($lis) => { + expect($lis, "3 items").to.have.length(3); + expect($lis.eq(0), "first item").to.contain("Walk the dog"); + expect($lis.eq(1), "second item").to.contain("Feed the cat"); + expect($lis.eq(2), "third item").to.contain("Write JavaScript"); + }); + }); + + it("yields the returned value to the next command", () => { + cy.wrap(1) + .then((num) => { + expect(num).to.equal(1); + + return 2; }) - }) + .then((num) => { + expect(num).to.equal(2); + }); + }); - describe('.then()', () => { - it('invokes a callback function with the current subject', () => { - // https://on.cypress.io/then - cy.get('.connectors-list > li') - .then(($lis) => { - expect($lis, '3 items').to.have.length(3) - expect($lis.eq(0), 'first item').to.contain('Walk the dog') - expect($lis.eq(1), 'second item').to.contain('Feed the cat') - expect($lis.eq(2), 'third item').to.contain('Write JavaScript') - }) + it("yields the original subject without return", () => { + cy.wrap(1) + .then((num) => { + expect(num).to.equal(1); + // note that nothing is returned from this callback }) + .then((num) => { + // this callback receives the original unchanged value 1 + expect(num).to.equal(1); + }); + }); - it('yields the returned value to the next command', () => { - cy.wrap(1) - .then((num) => { - expect(num).to.equal(1) - - return 2 - }) - .then((num) => { - expect(num).to.equal(2) - }) + it("yields the value yielded by the last Cypress command inside", () => { + cy.wrap(1) + .then((num) => { + expect(num).to.equal(1); + // note how we run a Cypress command + // the result yielded by this Cypress command + // will be passed to the second ".then" + cy.wrap(2); }) - - it('yields the original subject without return', () => { - cy.wrap(1) - .then((num) => { - expect(num).to.equal(1) - // note that nothing is returned from this callback - }) - .then((num) => { - // this callback receives the original unchanged value 1 - expect(num).to.equal(1) - }) - }) - - it('yields the value yielded by the last Cypress command inside', () => { - cy.wrap(1) - .then((num) => { - expect(num).to.equal(1) - // note how we run a Cypress command - // the result yielded by this Cypress command - // will be passed to the second ".then" - cy.wrap(2) - }) - .then((num) => { - // this callback receives the value yielded by "cy.wrap(2)" - expect(num).to.equal(2) - }) - }) - }) -}) + .then((num) => { + // this callback receives the value yielded by "cy.wrap(2)" + expect(num).to.equal(2); + }); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/cookies.cy.js b/client/cypress/e2e/2-advanced-examples/cookies.cy.js index b62341a36..390ee76e2 100644 --- a/client/cypress/e2e/2-advanced-examples/cookies.cy.js +++ b/client/cypress/e2e/2-advanced-examples/cookies.cy.js @@ -1,77 +1,79 @@ /// -context('Cookies', () => { - beforeEach(() => { - Cypress.Cookies.debug(true) +context("Cookies", () => { + beforeEach(() => { + Cypress.Cookies.debug(true); - cy.visit('https://example.cypress.io/commands/cookies') + cy.visit("https://example.cypress.io/commands/cookies"); - // clear cookies again after visiting to remove - // any 3rd party cookies picked up such as cloudflare - cy.clearCookies() - }) + // clear cookies again after visiting to remove + // any 3rd party cookies picked up such as cloudflare + cy.clearCookies(); + }); - it('cy.getCookie() - get a browser cookie', () => { - // https://on.cypress.io/getcookie - cy.get('#getCookie .set-a-cookie').click() + it("cy.getCookie() - get a browser cookie", () => { + // https://on.cypress.io/getcookie + cy.get("#getCookie .set-a-cookie").click(); - // cy.getCookie() yields a cookie object - cy.getCookie('token').should('have.property', 'value', '123ABC') - }) + // cy.getCookie() yields a cookie object + cy.getCookie("token").should("have.property", "value", "123ABC"); + }); - it('cy.getCookies() - get browser cookies', () => { - // https://on.cypress.io/getcookies - cy.getCookies().should('be.empty') + it("cy.getCookies() - get browser cookies", () => { + // https://on.cypress.io/getcookies + cy.getCookies().should("be.empty"); - cy.get('#getCookies .set-a-cookie').click() + cy.get("#getCookies .set-a-cookie").click(); - // cy.getCookies() yields an array of cookies - cy.getCookies().should('have.length', 1).should((cookies) => { - // each cookie has these properties - expect(cookies[0]).to.have.property('name', 'token') - expect(cookies[0]).to.have.property('value', '123ABC') - expect(cookies[0]).to.have.property('httpOnly', false) - expect(cookies[0]).to.have.property('secure', false) - expect(cookies[0]).to.have.property('domain') - expect(cookies[0]).to.have.property('path') - }) - }) + // cy.getCookies() yields an array of cookies + cy.getCookies() + .should("have.length", 1) + .should((cookies) => { + // each cookie has these properties + expect(cookies[0]).to.have.property("name", "token"); + expect(cookies[0]).to.have.property("value", "123ABC"); + expect(cookies[0]).to.have.property("httpOnly", false); + expect(cookies[0]).to.have.property("secure", false); + expect(cookies[0]).to.have.property("domain"); + expect(cookies[0]).to.have.property("path"); + }); + }); - it('cy.setCookie() - set a browser cookie', () => { - // https://on.cypress.io/setcookie - cy.getCookies().should('be.empty') + it("cy.setCookie() - set a browser cookie", () => { + // https://on.cypress.io/setcookie + cy.getCookies().should("be.empty"); - cy.setCookie('foo', 'bar') + cy.setCookie("foo", "bar"); - // cy.getCookie() yields a cookie object - cy.getCookie('foo').should('have.property', 'value', 'bar') - }) + // cy.getCookie() yields a cookie object + cy.getCookie("foo").should("have.property", "value", "bar"); + }); - it('cy.clearCookie() - clear a browser cookie', () => { - // https://on.cypress.io/clearcookie - cy.getCookie('token').should('be.null') + it("cy.clearCookie() - clear a browser cookie", () => { + // https://on.cypress.io/clearcookie + cy.getCookie("token").should("be.null"); - cy.get('#clearCookie .set-a-cookie').click() + cy.get("#clearCookie .set-a-cookie").click(); - cy.getCookie('token').should('have.property', 'value', '123ABC') + cy.getCookie("token").should("have.property", "value", "123ABC"); - // cy.clearCookies() yields null - cy.clearCookie('token').should('be.null') + // cy.clearCookies() yields null + cy.clearCookie("token").should("be.null"); - cy.getCookie('token').should('be.null') - }) + cy.getCookie("token").should("be.null"); + }); - it('cy.clearCookies() - clear browser cookies', () => { - // https://on.cypress.io/clearcookies - cy.getCookies().should('be.empty') + it("cy.clearCookies() - clear browser cookies", () => { + // https://on.cypress.io/clearcookies + cy.getCookies().should("be.empty"); - cy.get('#clearCookies .set-a-cookie').click() + cy.get("#clearCookies .set-a-cookie").click(); - cy.getCookies().should('have.length', 1) + cy.getCookies().should("have.length", 1); - // cy.clearCookies() yields null - cy.clearCookies() + // cy.clearCookies() yields null + cy.clearCookies(); - cy.getCookies().should('be.empty') - }) -}) + cy.getCookies().should("be.empty"); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js b/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js index 913d58f4a..1cd9f975e 100644 --- a/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js +++ b/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js @@ -1,202 +1,208 @@ /// -context('Cypress.Commands', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.Commands", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - // https://on.cypress.io/custom-commands + // https://on.cypress.io/custom-commands - it('.add() - create a custom command', () => { - Cypress.Commands.add('console', { - prevSubject: true, - }, (subject, method) => { - // the previous subject is automatically received - // and the commands arguments are shifted + it(".add() - create a custom command", () => { + Cypress.Commands.add( + "console", + { + prevSubject: true + }, + (subject, method) => { + // the previous subject is automatically received + // and the commands arguments are shifted - // allow us to change the console method used - method = method || 'log' + // allow us to change the console method used + method = method || "log"; - // log the subject to the console - // @ts-ignore TS7017 - console[method]('The subject is', subject) + // log the subject to the console + // @ts-ignore TS7017 + console[method]("The subject is", subject); - // whatever we return becomes the new subject - // we don't want to change the subject so - // we return whatever was passed in - return subject - }) + // whatever we return becomes the new subject + // we don't want to change the subject so + // we return whatever was passed in + return subject; + } + ); - // @ts-ignore TS2339 - cy.get('button').console('info').then(($button) => { - // subject is still $button - }) - }) -}) + // @ts-ignore TS2339 + cy.get("button") + .console("info") + .then(($button) => { + // subject is still $button + }); + }); +}); -context('Cypress.Cookies', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.Cookies", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - // https://on.cypress.io/cookies - it('.debug() - enable or disable debugging', () => { - Cypress.Cookies.debug(true) + // https://on.cypress.io/cookies + it(".debug() - enable or disable debugging", () => { + Cypress.Cookies.debug(true); - // Cypress will now log in the console when - // cookies are set or cleared - cy.setCookie('fakeCookie', '123ABC') - cy.clearCookie('fakeCookie') - cy.setCookie('fakeCookie', '123ABC') - cy.clearCookie('fakeCookie') - cy.setCookie('fakeCookie', '123ABC') - }) + // Cypress will now log in the console when + // cookies are set or cleared + cy.setCookie("fakeCookie", "123ABC"); + cy.clearCookie("fakeCookie"); + cy.setCookie("fakeCookie", "123ABC"); + cy.clearCookie("fakeCookie"); + cy.setCookie("fakeCookie", "123ABC"); + }); - it('.preserveOnce() - preserve cookies by key', () => { - // normally cookies are reset after each test - cy.getCookie('fakeCookie').should('not.be.ok') + it(".preserveOnce() - preserve cookies by key", () => { + // normally cookies are reset after each test + cy.getCookie("fakeCookie").should("not.be.ok"); - // preserving a cookie will not clear it when - // the next test starts - cy.setCookie('lastCookie', '789XYZ') - Cypress.Cookies.preserveOnce('lastCookie') - }) + // preserving a cookie will not clear it when + // the next test starts + cy.setCookie("lastCookie", "789XYZ"); + Cypress.Cookies.preserveOnce("lastCookie"); + }); - it('.defaults() - set defaults for all cookies', () => { - // now any cookie with the name 'session_id' will - // not be cleared before each new test runs - Cypress.Cookies.defaults({ - preserve: 'session_id', - }) - }) -}) + it(".defaults() - set defaults for all cookies", () => { + // now any cookie with the name 'session_id' will + // not be cleared before each new test runs + Cypress.Cookies.defaults({ + preserve: "session_id" + }); + }); +}); -context('Cypress.arch', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.arch", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Get CPU architecture name of underlying OS', () => { - // https://on.cypress.io/arch - expect(Cypress.arch).to.exist - }) -}) + it("Get CPU architecture name of underlying OS", () => { + // https://on.cypress.io/arch + expect(Cypress.arch).to.exist; + }); +}); -context('Cypress.config()', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.config()", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Get and set configuration options', () => { - // https://on.cypress.io/config - let myConfig = Cypress.config() + it("Get and set configuration options", () => { + // https://on.cypress.io/config + let myConfig = Cypress.config(); - expect(myConfig).to.have.property('animationDistanceThreshold', 5) - expect(myConfig).to.have.property('baseUrl', null) - expect(myConfig).to.have.property('defaultCommandTimeout', 4000) - expect(myConfig).to.have.property('requestTimeout', 5000) - expect(myConfig).to.have.property('responseTimeout', 30000) - expect(myConfig).to.have.property('viewportHeight', 660) - expect(myConfig).to.have.property('viewportWidth', 1000) - expect(myConfig).to.have.property('pageLoadTimeout', 60000) - expect(myConfig).to.have.property('waitForAnimations', true) + expect(myConfig).to.have.property("animationDistanceThreshold", 5); + expect(myConfig).to.have.property("baseUrl", null); + expect(myConfig).to.have.property("defaultCommandTimeout", 4000); + expect(myConfig).to.have.property("requestTimeout", 5000); + expect(myConfig).to.have.property("responseTimeout", 30000); + expect(myConfig).to.have.property("viewportHeight", 660); + expect(myConfig).to.have.property("viewportWidth", 1000); + expect(myConfig).to.have.property("pageLoadTimeout", 60000); + expect(myConfig).to.have.property("waitForAnimations", true); - expect(Cypress.config('pageLoadTimeout')).to.eq(60000) + expect(Cypress.config("pageLoadTimeout")).to.eq(60000); - // this will change the config for the rest of your tests! - Cypress.config('pageLoadTimeout', 20000) + // this will change the config for the rest of your tests! + Cypress.config("pageLoadTimeout", 20000); - expect(Cypress.config('pageLoadTimeout')).to.eq(20000) + expect(Cypress.config("pageLoadTimeout")).to.eq(20000); - Cypress.config('pageLoadTimeout', 60000) - }) -}) + Cypress.config("pageLoadTimeout", 60000); + }); +}); -context('Cypress.dom', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.dom", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - // https://on.cypress.io/dom - it('.isHidden() - determine if a DOM element is hidden', () => { - let hiddenP = Cypress.$('.dom-p p.hidden').get(0) - let visibleP = Cypress.$('.dom-p p.visible').get(0) + // https://on.cypress.io/dom + it(".isHidden() - determine if a DOM element is hidden", () => { + let hiddenP = Cypress.$(".dom-p p.hidden").get(0); + let visibleP = Cypress.$(".dom-p p.visible").get(0); - // our first paragraph has css class 'hidden' - expect(Cypress.dom.isHidden(hiddenP)).to.be.true - expect(Cypress.dom.isHidden(visibleP)).to.be.false - }) -}) + // our first paragraph has css class 'hidden' + expect(Cypress.dom.isHidden(hiddenP)).to.be.true; + expect(Cypress.dom.isHidden(visibleP)).to.be.false; + }); +}); -context('Cypress.env()', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.env()", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - // We can set environment variables for highly dynamic values + // We can set environment variables for highly dynamic values - // https://on.cypress.io/environment-variables - it('Get environment variables', () => { - // https://on.cypress.io/env - // set multiple environment variables - Cypress.env({ - host: 'veronica.dev.local', - api_server: 'http://localhost:8888/v1/', - }) + // https://on.cypress.io/environment-variables + it("Get environment variables", () => { + // https://on.cypress.io/env + // set multiple environment variables + Cypress.env({ + host: "veronica.dev.local", + api_server: "http://localhost:8888/v1/" + }); - // get environment variable - expect(Cypress.env('host')).to.eq('veronica.dev.local') + // get environment variable + expect(Cypress.env("host")).to.eq("veronica.dev.local"); - // set environment variable - Cypress.env('api_server', 'http://localhost:8888/v2/') - expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') + // set environment variable + Cypress.env("api_server", "http://localhost:8888/v2/"); + expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/"); - // get all environment variable - expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') - expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') - }) -}) + // get all environment variable + expect(Cypress.env()).to.have.property("host", "veronica.dev.local"); + expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/"); + }); +}); -context('Cypress.log', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.log", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Control what is printed to the Command Log', () => { - // https://on.cypress.io/cypress-log - }) -}) + it("Control what is printed to the Command Log", () => { + // https://on.cypress.io/cypress-log + }); +}); -context('Cypress.platform', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.platform", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Get underlying OS name', () => { - // https://on.cypress.io/platform - expect(Cypress.platform).to.be.exist - }) -}) + it("Get underlying OS name", () => { + // https://on.cypress.io/platform + expect(Cypress.platform).to.be.exist; + }); +}); -context('Cypress.version', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.version", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Get current version of Cypress being run', () => { - // https://on.cypress.io/version - expect(Cypress.version).to.be.exist - }) -}) + it("Get current version of Cypress being run", () => { + // https://on.cypress.io/version + expect(Cypress.version).to.be.exist; + }); +}); -context('Cypress.spec', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/cypress-api') - }) +context("Cypress.spec", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/cypress-api"); + }); - it('Get current spec information', () => { - // https://on.cypress.io/spec - // wrap the object so we can inspect it easily by clicking in the command log - cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute']) - }) -}) + it("Get current spec information", () => { + // https://on.cypress.io/spec + // wrap the object so we can inspect it easily by clicking in the command log + cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/files.cy.js b/client/cypress/e2e/2-advanced-examples/files.cy.js index 0c47da8bb..ad1bef656 100644 --- a/client/cypress/e2e/2-advanced-examples/files.cy.js +++ b/client/cypress/e2e/2-advanced-examples/files.cy.js @@ -3,86 +3,84 @@ /// JSON fixture file can be loaded directly using // the built-in JavaScript bundler // @ts-ignore -const requiredExample = require('../../fixtures/example') +const requiredExample = require("../../fixtures/example"); -context('Files', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/files') - }) +context("Files", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/files"); + }); - beforeEach(() => { - // load example.json fixture file and store - // in the test context object - cy.fixture('example.json').as('example') - }) + beforeEach(() => { + // load example.json fixture file and store + // in the test context object + cy.fixture("example.json").as("example"); + }); - it('cy.fixture() - load a fixture', () => { - // https://on.cypress.io/fixture + it("cy.fixture() - load a fixture", () => { + // https://on.cypress.io/fixture - // Instead of writing a response inline you can - // use a fixture file's content. + // Instead of writing a response inline you can + // use a fixture file's content. - // when application makes an Ajax request matching "GET **/comments/*" - // Cypress will intercept it and reply with the object in `example.json` fixture - cy.intercept('GET', '**/comments/*', {fixture: 'example.json'}).as('getComment') + // when application makes an Ajax request matching "GET **/comments/*" + // Cypress will intercept it and reply with the object in `example.json` fixture + cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment"); - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.fixture-btn').click() + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get(".fixture-btn").click(); - cy.wait('@getComment').its('response.body') - .should('have.property', 'name') - .and('include', 'Using fixtures to represent data') - }) + cy.wait("@getComment") + .its("response.body") + .should("have.property", "name") + .and("include", "Using fixtures to represent data"); + }); - it('cy.fixture() or require - load a fixture', function () { - // we are inside the "function () { ... }" - // callback and can use test context object "this" - // "this.example" was loaded in "beforeEach" function callback - expect(this.example, 'fixture in the test context') - .to.deep.equal(requiredExample) + it("cy.fixture() or require - load a fixture", function () { + // we are inside the "function () { ... }" + // callback and can use test context object "this" + // "this.example" was loaded in "beforeEach" function callback + expect(this.example, "fixture in the test context").to.deep.equal(requiredExample); - // or use "cy.wrap" and "should('deep.equal', ...)" assertion - cy.wrap(this.example) - .should('deep.equal', requiredExample) - }) + // or use "cy.wrap" and "should('deep.equal', ...)" assertion + cy.wrap(this.example).should("deep.equal", requiredExample); + }); - it('cy.readFile() - read file contents', () => { - // https://on.cypress.io/readfile + it("cy.readFile() - read file contents", () => { + // https://on.cypress.io/readfile - // You can read a file and yield its contents - // The filePath is relative to your project's root. - cy.readFile('cypress.json').then((json) => { - expect(json).to.be.an('object') - }) - }) + // You can read a file and yield its contents + // The filePath is relative to your project's root. + cy.readFile("cypress.json").then((json) => { + expect(json).to.be.an("object"); + }); + }); - it('cy.writeFile() - write to a file', () => { - // https://on.cypress.io/writefile + it("cy.writeFile() - write to a file", () => { + // https://on.cypress.io/writefile - // You can write to a file + // You can write to a file - // Use a response from a request to automatically - // generate a fixture file for use later - cy.request('https://jsonplaceholder.cypress.io/users') - .then((response) => { - cy.writeFile('cypress/fixtures/users.json', response.body) - }) + // Use a response from a request to automatically + // generate a fixture file for use later + cy.request("https://jsonplaceholder.cypress.io/users").then((response) => { + cy.writeFile("cypress/fixtures/users.json", response.body); + }); - cy.fixture('users').should((users) => { - expect(users[0].name).to.exist - }) + cy.fixture("users").should((users) => { + expect(users[0].name).to.exist; + }); - // JavaScript arrays and objects are stringified - // and formatted into text. - cy.writeFile('cypress/fixtures/profile.json', { - id: 8739, - name: 'Jane', - email: 'jane@example.com', - }) + // JavaScript arrays and objects are stringified + // and formatted into text. + cy.writeFile("cypress/fixtures/profile.json", { + id: 8739, + name: "Jane", + email: "jane@example.com" + }); - cy.fixture('profile').should((profile) => { - expect(profile.name).to.eq('Jane') - }) - }) -}) + cy.fixture("profile").should((profile) => { + expect(profile.name).to.eq("Jane"); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/local_storage.cy.js b/client/cypress/e2e/2-advanced-examples/local_storage.cy.js index b44a014c9..7007d02bf 100644 --- a/client/cypress/e2e/2-advanced-examples/local_storage.cy.js +++ b/client/cypress/e2e/2-advanced-examples/local_storage.cy.js @@ -1,52 +1,58 @@ /// -context('Local Storage', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/local-storage') - }) - // Although local storage is automatically cleared - // in between tests to maintain a clean state - // sometimes we need to clear the local storage manually +context("Local Storage", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/local-storage"); + }); + // Although local storage is automatically cleared + // in between tests to maintain a clean state + // sometimes we need to clear the local storage manually - it('cy.clearLocalStorage() - clear all data in local storage', () => { - // https://on.cypress.io/clearlocalstorage - cy.get('.ls-btn').click().should(() => { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) + it("cy.clearLocalStorage() - clear all data in local storage", () => { + // https://on.cypress.io/clearlocalstorage + cy.get(".ls-btn") + .click() + .should(() => { + expect(localStorage.getItem("prop1")).to.eq("red"); + expect(localStorage.getItem("prop2")).to.eq("blue"); + expect(localStorage.getItem("prop3")).to.eq("magenta"); + }); - // clearLocalStorage() yields the localStorage object - cy.clearLocalStorage().should((ls) => { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.be.null - expect(ls.getItem('prop3')).to.be.null - }) + // clearLocalStorage() yields the localStorage object + cy.clearLocalStorage().should((ls) => { + expect(ls.getItem("prop1")).to.be.null; + expect(ls.getItem("prop2")).to.be.null; + expect(ls.getItem("prop3")).to.be.null; + }); - cy.get('.ls-btn').click().should(() => { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) + cy.get(".ls-btn") + .click() + .should(() => { + expect(localStorage.getItem("prop1")).to.eq("red"); + expect(localStorage.getItem("prop2")).to.eq("blue"); + expect(localStorage.getItem("prop3")).to.eq("magenta"); + }); - // Clear key matching string in Local Storage - cy.clearLocalStorage('prop1').should((ls) => { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.eq('blue') - expect(ls.getItem('prop3')).to.eq('magenta') - }) + // Clear key matching string in Local Storage + cy.clearLocalStorage("prop1").should((ls) => { + expect(ls.getItem("prop1")).to.be.null; + expect(ls.getItem("prop2")).to.eq("blue"); + expect(ls.getItem("prop3")).to.eq("magenta"); + }); - cy.get('.ls-btn').click().should(() => { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) + cy.get(".ls-btn") + .click() + .should(() => { + expect(localStorage.getItem("prop1")).to.eq("red"); + expect(localStorage.getItem("prop2")).to.eq("blue"); + expect(localStorage.getItem("prop3")).to.eq("magenta"); + }); - // Clear keys matching regex in Local Storage - cy.clearLocalStorage(/prop1|2/).should((ls) => { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.be.null - expect(ls.getItem('prop3')).to.eq('magenta') - }) - }) -}) + // Clear keys matching regex in Local Storage + cy.clearLocalStorage(/prop1|2/).should((ls) => { + expect(ls.getItem("prop1")).to.be.null; + expect(ls.getItem("prop2")).to.be.null; + expect(ls.getItem("prop3")).to.eq("magenta"); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/location.cy.js b/client/cypress/e2e/2-advanced-examples/location.cy.js index 165cdec94..f5e6230d6 100644 --- a/client/cypress/e2e/2-advanced-examples/location.cy.js +++ b/client/cypress/e2e/2-advanced-examples/location.cy.js @@ -1,32 +1,32 @@ /// -context('Location', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/location') - }) +context("Location", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/location"); + }); - it('cy.hash() - get the current URL hash', () => { - // https://on.cypress.io/hash - cy.hash().should('be.empty') - }) + it("cy.hash() - get the current URL hash", () => { + // https://on.cypress.io/hash + cy.hash().should("be.empty"); + }); - it('cy.location() - get window.location', () => { - // https://on.cypress.io/location - cy.location().should((location) => { - expect(location.hash).to.be.empty - expect(location.href).to.eq('https://example.cypress.io/commands/location') - expect(location.host).to.eq('example.cypress.io') - expect(location.hostname).to.eq('example.cypress.io') - expect(location.origin).to.eq('https://example.cypress.io') - expect(location.pathname).to.eq('/commands/location') - expect(location.port).to.eq('') - expect(location.protocol).to.eq('https:') - expect(location.search).to.be.empty - }) - }) + it("cy.location() - get window.location", () => { + // https://on.cypress.io/location + cy.location().should((location) => { + expect(location.hash).to.be.empty; + expect(location.href).to.eq("https://example.cypress.io/commands/location"); + expect(location.host).to.eq("example.cypress.io"); + expect(location.hostname).to.eq("example.cypress.io"); + expect(location.origin).to.eq("https://example.cypress.io"); + expect(location.pathname).to.eq("/commands/location"); + expect(location.port).to.eq(""); + expect(location.protocol).to.eq("https:"); + expect(location.search).to.be.empty; + }); + }); - it('cy.url() - get the current URL', () => { - // https://on.cypress.io/url - cy.url().should('eq', 'https://example.cypress.io/commands/location') - }) -}) + it("cy.url() - get the current URL", () => { + // https://on.cypress.io/url + cy.url().should("eq", "https://example.cypress.io/commands/location"); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/misc.cy.js b/client/cypress/e2e/2-advanced-examples/misc.cy.js index 905261824..9ef98ae41 100644 --- a/client/cypress/e2e/2-advanced-examples/misc.cy.js +++ b/client/cypress/e2e/2-advanced-examples/misc.cy.js @@ -1,106 +1,98 @@ /// -context('Misc', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/misc') - }) +context("Misc", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/misc"); + }); - it('.end() - end the command chain', () => { - // https://on.cypress.io/end + it(".end() - end the command chain", () => { + // https://on.cypress.io/end - // cy.end is useful when you want to end a chain of commands - // and force Cypress to re-query from the root element - cy.get('.misc-table').within(() => { - // ends the current chain and yields null - cy.contains('Cheryl').click().end() + // cy.end is useful when you want to end a chain of commands + // and force Cypress to re-query from the root element + cy.get(".misc-table").within(() => { + // ends the current chain and yields null + cy.contains("Cheryl").click().end(); - // queries the entire table again - cy.contains('Charles').click() - }) - }) + // queries the entire table again + cy.contains("Charles").click(); + }); + }); - it('cy.exec() - execute a system command', () => { - // execute a system command. - // so you can take actions necessary for - // your test outside the scope of Cypress. - // https://on.cypress.io/exec + it("cy.exec() - execute a system command", () => { + // execute a system command. + // so you can take actions necessary for + // your test outside the scope of Cypress. + // https://on.cypress.io/exec - // we can use Cypress.platform string to - // select appropriate command - // https://on.cypress/io/platform - cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) + // we can use Cypress.platform string to + // select appropriate command + // https://on.cypress/io/platform + cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`); - // on CircleCI Windows build machines we have a failure to run bash shell - // https://github.com/cypress-io/cypress/issues/5169 - // so skip some of the tests by passing flag "--env circle=true" - const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') + // on CircleCI Windows build machines we have a failure to run bash shell + // https://github.com/cypress-io/cypress/issues/5169 + // so skip some of the tests by passing flag "--env circle=true" + const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle"); - if (isCircleOnWindows) { - cy.log('Skipping test on CircleCI') + if (isCircleOnWindows) { + cy.log("Skipping test on CircleCI"); - return - } + return; + } - // cy.exec problem on Shippable CI - // https://github.com/cypress-io/cypress/issues/6718 - const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') + // cy.exec problem on Shippable CI + // https://github.com/cypress-io/cypress/issues/6718 + const isShippable = Cypress.platform === "linux" && Cypress.env("shippable"); - if (isShippable) { - cy.log('Skipping test on ShippableCI') + if (isShippable) { + cy.log("Skipping test on ShippableCI"); - return - } + return; + } - cy.exec('echo Jane Lane') - .its('stdout').should('contain', 'Jane Lane') + cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane"); - if (Cypress.platform === 'win32') { - cy.exec('print cypress.json') - .its('stderr').should('be.empty') - } else { - cy.exec('cat cypress.json') - .its('stderr').should('be.empty') + if (Cypress.platform === "win32") { + cy.exec("print cypress.json").its("stderr").should("be.empty"); + } else { + cy.exec("cat cypress.json").its("stderr").should("be.empty"); - cy.exec('pwd') - .its('code').should('eq', 0) - } - }) + cy.exec("pwd").its("code").should("eq", 0); + } + }); - it('cy.focused() - get the DOM element that has focus', () => { - // https://on.cypress.io/focused - cy.get('.misc-form').find('#name').click() - cy.focused().should('have.id', 'name') + it("cy.focused() - get the DOM element that has focus", () => { + // https://on.cypress.io/focused + cy.get(".misc-form").find("#name").click(); + cy.focused().should("have.id", "name"); - cy.get('.misc-form').find('#description').click() - cy.focused().should('have.id', 'description') - }) + cy.get(".misc-form").find("#description").click(); + cy.focused().should("have.id", "description"); + }); - context('Cypress.Screenshot', function () { - it('cy.screenshot() - take a screenshot', () => { - // https://on.cypress.io/screenshot - cy.screenshot('my-image') - }) + context("Cypress.Screenshot", function () { + it("cy.screenshot() - take a screenshot", () => { + // https://on.cypress.io/screenshot + cy.screenshot("my-image"); + }); - it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { - Cypress.Screenshot.defaults({ - blackout: ['.foo'], - capture: 'viewport', - clip: {x: 0, y: 0, width: 200, height: 200}, - scale: false, - disableTimersAndAnimations: true, - screenshotOnRunFailure: true, - onBeforeScreenshot() { - }, - onAfterScreenshot() { - }, - }) - }) - }) + it("Cypress.Screenshot.defaults() - change default config of screenshots", function () { + Cypress.Screenshot.defaults({ + blackout: [".foo"], + capture: "viewport", + clip: { x: 0, y: 0, width: 200, height: 200 }, + scale: false, + disableTimersAndAnimations: true, + screenshotOnRunFailure: true, + onBeforeScreenshot() {}, + onAfterScreenshot() {} + }); + }); + }); - it('cy.wrap() - wrap an object', () => { - // https://on.cypress.io/wrap - cy.wrap({foo: 'bar'}) - .should('have.property', 'foo') - .and('include', 'bar') - }) -}) + it("cy.wrap() - wrap an object", () => { + // https://on.cypress.io/wrap + cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar"); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/navigation.cy.js b/client/cypress/e2e/2-advanced-examples/navigation.cy.js index 55303f309..a1993ee36 100644 --- a/client/cypress/e2e/2-advanced-examples/navigation.cy.js +++ b/client/cypress/e2e/2-advanced-examples/navigation.cy.js @@ -1,56 +1,56 @@ /// -context('Navigation', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io') - cy.get('.navbar-nav').contains('Commands').click() - cy.get('.dropdown-menu').contains('Navigation').click() - }) +context("Navigation", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io"); + cy.get(".navbar-nav").contains("Commands").click(); + cy.get(".dropdown-menu").contains("Navigation").click(); + }); - it('cy.go() - go back or forward in the browser\'s history', () => { - // https://on.cypress.io/go + it("cy.go() - go back or forward in the browser's history", () => { + // https://on.cypress.io/go - cy.location('pathname').should('include', 'navigation') + cy.location("pathname").should("include", "navigation"); - cy.go('back') - cy.location('pathname').should('not.include', 'navigation') + cy.go("back"); + cy.location("pathname").should("not.include", "navigation"); - cy.go('forward') - cy.location('pathname').should('include', 'navigation') + cy.go("forward"); + cy.location("pathname").should("include", "navigation"); - // clicking back - cy.go(-1) - cy.location('pathname').should('not.include', 'navigation') + // clicking back + cy.go(-1); + cy.location("pathname").should("not.include", "navigation"); - // clicking forward - cy.go(1) - cy.location('pathname').should('include', 'navigation') - }) + // clicking forward + cy.go(1); + cy.location("pathname").should("include", "navigation"); + }); - it('cy.reload() - reload the page', () => { - // https://on.cypress.io/reload - cy.reload() + it("cy.reload() - reload the page", () => { + // https://on.cypress.io/reload + cy.reload(); - // reload the page without using the cache - cy.reload(true) - }) + // reload the page without using the cache + cy.reload(true); + }); - it('cy.visit() - visit a remote url', () => { - // https://on.cypress.io/visit + it("cy.visit() - visit a remote url", () => { + // https://on.cypress.io/visit - // Visit any sub-domain of your current domain + // Visit any sub-domain of your current domain - // Pass options to the visit - cy.visit('https://example.cypress.io/commands/navigation', { - timeout: 50000, // increase total time for the visit to resolve - onBeforeLoad(contentWindow) { - // contentWindow is the remote page's window object - expect(typeof contentWindow === 'object').to.be.true - }, - onLoad(contentWindow) { - // contentWindow is the remote page's window object - expect(typeof contentWindow === 'object').to.be.true - }, - }) - }) -}) + // Pass options to the visit + cy.visit("https://example.cypress.io/commands/navigation", { + timeout: 50000, // increase total time for the visit to resolve + onBeforeLoad(contentWindow) { + // contentWindow is the remote page's window object + expect(typeof contentWindow === "object").to.be.true; + }, + onLoad(contentWindow) { + // contentWindow is the remote page's window object + expect(typeof contentWindow === "object").to.be.true; + } + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/network_requests.cy.js b/client/cypress/e2e/2-advanced-examples/network_requests.cy.js index 02c2ae17a..693e5395d 100644 --- a/client/cypress/e2e/2-advanced-examples/network_requests.cy.js +++ b/client/cypress/e2e/2-advanced-examples/network_requests.cy.js @@ -1,163 +1,165 @@ /// -context('Network Requests', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/network-requests') +context("Network Requests", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/network-requests"); + }); + + // Manage HTTP requests in your app + + it("cy.request() - make an XHR request", () => { + // https://on.cypress.io/request + cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => { + expect(response.status).to.eq(200); + // the server sometimes gets an extra comment posted from another machine + // which gets returned as 1 extra object + expect(response.body).to.have.property("length").and.be.oneOf([500, 501]); + expect(response).to.have.property("headers"); + expect(response).to.have.property("duration"); + }); + }); + + it("cy.request() - verify response using BDD syntax", () => { + cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => { + // https://on.cypress.io/assertions + expect(response).property("status").to.equal(200); + expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]); + expect(response).to.include.keys("headers", "duration"); + }); + }); + + it("cy.request() with query parameters", () => { + // will execute request + // https://jsonplaceholder.cypress.io/comments?postId=1&id=3 + cy.request({ + url: "https://jsonplaceholder.cypress.io/comments", + qs: { + postId: 1, + id: 3 + } }) + .its("body") + .should("be.an", "array") + .and("have.length", 1) + .its("0") // yields first element of the array + .should("contain", { + postId: 1, + id: 3 + }); + }); - // Manage HTTP requests in your app + it("cy.request() - pass result to the second request", () => { + // first, let's find out the userId of the first user we have + cy.request("https://jsonplaceholder.cypress.io/users?_limit=1") + .its("body") // yields the response object + .its("0") // yields the first element of the returned list + // the above two commands its('body').its('0') + // can be written as its('body.0') + // if you do not care about TypeScript checks + .then((user) => { + expect(user).property("id").to.be.a("number"); + // make a new post on behalf of the user + cy.request("POST", "https://jsonplaceholder.cypress.io/posts", { + userId: user.id, + title: "Cypress Test Runner", + body: "Fast, easy and reliable testing for anything that runs in a browser." + }); + }) + // note that the value here is the returned value of the 2nd request + // which is the new post object + .then((response) => { + expect(response).property("status").to.equal(201); // new entity created + expect(response).property("body").to.contain({ + title: "Cypress Test Runner" + }); - it('cy.request() - make an XHR request', () => { - // https://on.cypress.io/request - cy.request('https://jsonplaceholder.cypress.io/comments') - .should((response) => { - expect(response.status).to.eq(200) - // the server sometimes gets an extra comment posted from another machine - // which gets returned as 1 extra object - expect(response.body).to.have.property('length').and.be.oneOf([500, 501]) - expect(response).to.have.property('headers') - expect(response).to.have.property('duration') - }) - }) + // we don't know the exact post id - only that it will be > 100 + // since JSONPlaceholder has built-in 100 posts + expect(response.body).property("id").to.be.a("number").and.to.be.gt(100); - it('cy.request() - verify response using BDD syntax', () => { - cy.request('https://jsonplaceholder.cypress.io/comments') - .then((response) => { - // https://on.cypress.io/assertions - expect(response).property('status').to.equal(200) - expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501]) - expect(response).to.include.keys('headers', 'duration') - }) - }) + // we don't know the user id here - since it was in above closure + // so in this test just confirm that the property is there + expect(response.body).property("userId").to.be.a("number"); + }); + }); - it('cy.request() with query parameters', () => { - // will execute request - // https://jsonplaceholder.cypress.io/comments?postId=1&id=3 - cy.request({ - url: 'https://jsonplaceholder.cypress.io/comments', - qs: { - postId: 1, - id: 3, - }, + it("cy.request() - save response in the shared test context", () => { + // https://on.cypress.io/variables-and-aliases + cy.request("https://jsonplaceholder.cypress.io/users?_limit=1") + .its("body") + .its("0") // yields the first element of the returned list + .as("user") // saves the object in the test context + .then(function () { + // NOTE 👀 + // By the time this callback runs the "as('user')" command + // has saved the user object in the test context. + // To access the test context we need to use + // the "function () { ... }" callback form, + // otherwise "this" points at a wrong or undefined object! + cy.request("POST", "https://jsonplaceholder.cypress.io/posts", { + userId: this.user.id, + title: "Cypress Test Runner", + body: "Fast, easy and reliable testing for anything that runs in a browser." }) - .its('body') - .should('be.an', 'array') - .and('have.length', 1) - .its('0') // yields first element of the array - .should('contain', { - postId: 1, - id: 3, - }) - }) + .its("body") + .as("post"); // save the new post from the response + }) + .then(function () { + // When this callback runs, both "cy.request" API commands have finished + // and the test context has "user" and "post" objects set. + // Let's verify them. + expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id); + }); + }); - it('cy.request() - pass result to the second request', () => { - // first, let's find out the userId of the first user we have - cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') - .its('body') // yields the response object - .its('0') // yields the first element of the returned list - // the above two commands its('body').its('0') - // can be written as its('body.0') - // if you do not care about TypeScript checks - .then((user) => { - expect(user).property('id').to.be.a('number') - // make a new post on behalf of the user - cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { - userId: user.id, - title: 'Cypress Test Runner', - body: 'Fast, easy and reliable testing for anything that runs in a browser.', - }) - }) - // note that the value here is the returned value of the 2nd request - // which is the new post object - .then((response) => { - expect(response).property('status').to.equal(201) // new entity created - expect(response).property('body').to.contain({ - title: 'Cypress Test Runner', - }) + it("cy.intercept() - route responses to matching requests", () => { + // https://on.cypress.io/intercept - // we don't know the exact post id - only that it will be > 100 - // since JSONPlaceholder has built-in 100 posts - expect(response.body).property('id').to.be.a('number') - .and.to.be.gt(100) + let message = "whoa, this comment does not exist"; - // we don't know the user id here - since it was in above closure - // so in this test just confirm that the property is there - expect(response.body).property('userId').to.be.a('number') - }) - }) + // Listen to GET to comments/1 + cy.intercept("GET", "**/comments/*").as("getComment"); - it('cy.request() - save response in the shared test context', () => { - // https://on.cypress.io/variables-and-aliases - cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') - .its('body').its('0') // yields the first element of the returned list - .as('user') // saves the object in the test context - .then(function () { - // NOTE 👀 - // By the time this callback runs the "as('user')" command - // has saved the user object in the test context. - // To access the test context we need to use - // the "function () { ... }" callback form, - // otherwise "this" points at a wrong or undefined object! - cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { - userId: this.user.id, - title: 'Cypress Test Runner', - body: 'Fast, easy and reliable testing for anything that runs in a browser.', - }) - .its('body').as('post') // save the new post from the response - }) - .then(function () { - // When this callback runs, both "cy.request" API commands have finished - // and the test context has "user" and "post" objects set. - // Let's verify them. - expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id) - }) - }) + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get(".network-btn").click(); - it('cy.intercept() - route responses to matching requests', () => { - // https://on.cypress.io/intercept + // https://on.cypress.io/wait + cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]); - let message = 'whoa, this comment does not exist' + // Listen to POST to comments + cy.intercept("POST", "**/comments").as("postComment"); - // Listen to GET to comments/1 - cy.intercept('GET', '**/comments/*').as('getComment') + // we have code that posts a comment when + // the button is clicked in scripts.js + cy.get(".network-post").click(); + cy.wait("@postComment").should(({ request, response }) => { + expect(request.body).to.include("email"); + expect(request.headers).to.have.property("content-type"); + expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()"); + }); - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.network-btn').click() + // Stub a response to PUT comments/ **** + cy.intercept( + { + method: "PUT", + url: "**/comments/*" + }, + { + statusCode: 404, + body: { error: message }, + headers: { "access-control-allow-origin": "*" }, + delayMs: 500 + } + ).as("putComment"); - // https://on.cypress.io/wait - cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) + // we have code that puts a comment when + // the button is clicked in scripts.js + cy.get(".network-put").click(); - // Listen to POST to comments - cy.intercept('POST', '**/comments').as('postComment') + cy.wait("@putComment"); - // we have code that posts a comment when - // the button is clicked in scripts.js - cy.get('.network-post').click() - cy.wait('@postComment').should(({request, response}) => { - expect(request.body).to.include('email') - expect(request.headers).to.have.property('content-type') - expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()') - }) - - // Stub a response to PUT comments/ **** - cy.intercept({ - method: 'PUT', - url: '**/comments/*', - }, { - statusCode: 404, - body: {error: message}, - headers: {'access-control-allow-origin': '*'}, - delayMs: 500, - }).as('putComment') - - // we have code that puts a comment when - // the button is clicked in scripts.js - cy.get('.network-put').click() - - cy.wait('@putComment') - - // our 404 statusCode logic in scripts.js executed - cy.get('.network-put-comment').should('contain', message) - }) -}) + // our 404 statusCode logic in scripts.js executed + cy.get(".network-put-comment").should("contain", message); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/querying.cy.js b/client/cypress/e2e/2-advanced-examples/querying.cy.js index 809ccd675..35cf7c24a 100644 --- a/client/cypress/e2e/2-advanced-examples/querying.cy.js +++ b/client/cypress/e2e/2-advanced-examples/querying.cy.js @@ -1,114 +1,100 @@ /// -context('Querying', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/querying') - }) +context("Querying", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/querying"); + }); - // The most commonly used query is 'cy.get()', you can - // think of this like the '$' in jQuery + // The most commonly used query is 'cy.get()', you can + // think of this like the '$' in jQuery - it('cy.get() - query DOM elements', () => { - // https://on.cypress.io/get + it("cy.get() - query DOM elements", () => { + // https://on.cypress.io/get - cy.get('#query-btn').should('contain', 'Button') + cy.get("#query-btn").should("contain", "Button"); - cy.get('.query-btn').should('contain', 'Button') + cy.get(".query-btn").should("contain", "Button"); - cy.get('#querying .well>button:first').should('contain', 'Button') - // ↲ - // Use CSS selectors just like jQuery + cy.get("#querying .well>button:first").should("contain", "Button"); + // ↲ + // Use CSS selectors just like jQuery - cy.get('[data-test-id="test-example"]').should('have.class', 'example') + cy.get('[data-test-id="test-example"]').should("have.class", "example"); - // 'cy.get()' yields jQuery object, you can get its attribute - // by invoking `.attr()` method - cy.get('[data-test-id="test-example"]') - .invoke('attr', 'data-test-id') - .should('equal', 'test-example') + // 'cy.get()' yields jQuery object, you can get its attribute + // by invoking `.attr()` method + cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example"); - // or you can get element's CSS property - cy.get('[data-test-id="test-example"]') - .invoke('css', 'position') - .should('equal', 'static') + // or you can get element's CSS property + cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static"); - // or use assertions directly during 'cy.get()' - // https://on.cypress.io/assertions - cy.get('[data-test-id="test-example"]') - .should('have.attr', 'data-test-id', 'test-example') - .and('have.css', 'position', 'static') - }) + // or use assertions directly during 'cy.get()' + // https://on.cypress.io/assertions + cy.get('[data-test-id="test-example"]') + .should("have.attr", "data-test-id", "test-example") + .and("have.css", "position", "static"); + }); - it('cy.contains() - query DOM elements with matching content', () => { - // https://on.cypress.io/contains - cy.get('.query-list') - .contains('bananas') - .should('have.class', 'third') + it("cy.contains() - query DOM elements with matching content", () => { + // https://on.cypress.io/contains + cy.get(".query-list").contains("bananas").should("have.class", "third"); - // we can pass a regexp to `.contains()` - cy.get('.query-list') - .contains(/^b\w+/) - .should('have.class', 'third') + // we can pass a regexp to `.contains()` + cy.get(".query-list").contains(/^b\w+/).should("have.class", "third"); - cy.get('.query-list') - .contains('apples') - .should('have.class', 'first') + cy.get(".query-list").contains("apples").should("have.class", "first"); - // passing a selector to contains will - // yield the selector containing the text - cy.get('#querying') - .contains('ul', 'oranges') - .should('have.class', 'query-list') + // passing a selector to contains will + // yield the selector containing the text + cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list"); - cy.get('.query-button') - .contains('Save Form') - .should('have.class', 'btn') - }) + cy.get(".query-button").contains("Save Form").should("have.class", "btn"); + }); - it('.within() - query DOM elements within a specific element', () => { - // https://on.cypress.io/within - cy.get('.query-form').within(() => { - cy.get('input:first').should('have.attr', 'placeholder', 'Email') - cy.get('input:last').should('have.attr', 'placeholder', 'Password') - }) - }) + it(".within() - query DOM elements within a specific element", () => { + // https://on.cypress.io/within + cy.get(".query-form").within(() => { + cy.get("input:first").should("have.attr", "placeholder", "Email"); + cy.get("input:last").should("have.attr", "placeholder", "Password"); + }); + }); - it('cy.root() - query the root DOM element', () => { - // https://on.cypress.io/root + it("cy.root() - query the root DOM element", () => { + // https://on.cypress.io/root - // By default, root is the document - cy.root().should('match', 'html') + // By default, root is the document + cy.root().should("match", "html"); - cy.get('.query-ul').within(() => { - // In this within, the root is now the ul DOM element - cy.root().should('have.class', 'query-ul') - }) - }) + cy.get(".query-ul").within(() => { + // In this within, the root is now the ul DOM element + cy.root().should("have.class", "query-ul"); + }); + }); - it('best practices - selecting elements', () => { - // https://on.cypress.io/best-practices#Selecting-Elements - cy.get('[data-cy=best-practices-selecting-elements]').within(() => { - // Worst - too generic, no context - cy.get('button').click() + it("best practices - selecting elements", () => { + // https://on.cypress.io/best-practices#Selecting-Elements + cy.get("[data-cy=best-practices-selecting-elements]").within(() => { + // Worst - too generic, no context + cy.get("button").click(); - // Bad. Coupled to styling. Highly subject to change. - cy.get('.btn.btn-large').click() + // Bad. Coupled to styling. Highly subject to change. + cy.get(".btn.btn-large").click(); - // Average. Coupled to the `name` attribute which has HTML semantics. - cy.get('[name=submission]').click() + // Average. Coupled to the `name` attribute which has HTML semantics. + cy.get("[name=submission]").click(); - // Better. But still coupled to styling or JS event listeners. - cy.get('#main').click() + // Better. But still coupled to styling or JS event listeners. + cy.get("#main").click(); - // Slightly better. Uses an ID but also ensures the element - // has an ARIA role attribute - cy.get('#main[role=button]').click() + // Slightly better. Uses an ID but also ensures the element + // has an ARIA role attribute + cy.get("#main[role=button]").click(); - // Much better. But still coupled to text content that may change. - cy.contains('Submit').click() + // Much better. But still coupled to text content that may change. + cy.contains("Submit").click(); - // Best. Insulated from all changes. - cy.get('[data-cy=submit]').click() - }) - }) -}) + // Best. Insulated from all changes. + cy.get("[data-cy=submit]").click(); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js b/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js index 4f2479f15..7c86af8fa 100644 --- a/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js +++ b/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js @@ -2,205 +2,202 @@ // remove no check once Cypress.sinon is typed // https://github.com/cypress-io/cypress/issues/6720 -context('Spies, Stubs, and Clock', () => { - it('cy.spy() - wrap a method in a spy', () => { - // https://on.cypress.io/spy - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') +context("Spies, Stubs, and Clock", () => { + it("cy.spy() - wrap a method in a spy", () => { + // https://on.cypress.io/spy + cy.visit("https://example.cypress.io/commands/spies-stubs-clocks"); - const obj = { - foo() { - }, - } + const obj = { + foo() {} + }; - const spy = cy.spy(obj, 'foo').as('anyArgs') + const spy = cy.spy(obj, "foo").as("anyArgs"); - obj.foo() + obj.foo(); - expect(spy).to.be.called - }) + expect(spy).to.be.called; + }); - it('cy.spy() retries until assertions pass', () => { - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + it("cy.spy() retries until assertions pass", () => { + cy.visit("https://example.cypress.io/commands/spies-stubs-clocks"); - const obj = { - /** - * Prints the argument passed - * @param x {any} - */ - foo(x) { - console.log('obj.foo called with', x) - }, - } + const obj = { + /** + * Prints the argument passed + * @param x {any} + */ + foo(x) { + console.log("obj.foo called with", x); + } + }; - cy.spy(obj, 'foo').as('foo') + cy.spy(obj, "foo").as("foo"); - setTimeout(() => { - obj.foo('first') - }, 500) + setTimeout(() => { + obj.foo("first"); + }, 500); - setTimeout(() => { - obj.foo('second') - }, 2500) + setTimeout(() => { + obj.foo("second"); + }, 2500); - cy.get('@foo').should('have.been.calledTwice') - }) + cy.get("@foo").should("have.been.calledTwice"); + }); - it('cy.stub() - create a stub and/or replace a function with stub', () => { - // https://on.cypress.io/stub - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + it("cy.stub() - create a stub and/or replace a function with stub", () => { + // https://on.cypress.io/stub + cy.visit("https://example.cypress.io/commands/spies-stubs-clocks"); - const obj = { - /** - * prints both arguments to the console - * @param a {string} - * @param b {string} - */ - foo(a, b) { - console.log('a', a, 'b', b) - }, - } + const obj = { + /** + * prints both arguments to the console + * @param a {string} + * @param b {string} + */ + foo(a, b) { + console.log("a", a, "b", b); + } + }; - const stub = cy.stub(obj, 'foo').as('foo') + const stub = cy.stub(obj, "foo").as("foo"); - obj.foo('foo', 'bar') + obj.foo("foo", "bar"); - expect(stub).to.be.called - }) + expect(stub).to.be.called; + }); - it('cy.clock() - control time in the browser', () => { - // https://on.cypress.io/clock + it("cy.clock() - control time in the browser", () => { + // https://on.cypress.io/clock - // create the date in UTC so its always the same - // no matter what local timezone the browser is running in - const now = new Date(Date.UTC(2017, 2, 14)).getTime() + // create the date in UTC so its always the same + // no matter what local timezone the browser is running in + const now = new Date(Date.UTC(2017, 2, 14)).getTime(); - cy.clock(now) - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - cy.get('#clock-div').click() - .should('have.text', '1489449600') - }) + cy.clock(now); + cy.visit("https://example.cypress.io/commands/spies-stubs-clocks"); + cy.get("#clock-div").click().should("have.text", "1489449600"); + }); - it('cy.tick() - move time in the browser', () => { - // https://on.cypress.io/tick + it("cy.tick() - move time in the browser", () => { + // https://on.cypress.io/tick - // create the date in UTC so its always the same - // no matter what local timezone the browser is running in - const now = new Date(Date.UTC(2017, 2, 14)).getTime() + // create the date in UTC so its always the same + // no matter what local timezone the browser is running in + const now = new Date(Date.UTC(2017, 2, 14)).getTime(); - cy.clock(now) - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - cy.get('#tick-div').click() - .should('have.text', '1489449600') + cy.clock(now); + cy.visit("https://example.cypress.io/commands/spies-stubs-clocks"); + cy.get("#tick-div").click().should("have.text", "1489449600"); - cy.tick(10000) // 10 seconds passed - cy.get('#tick-div').click() - .should('have.text', '1489449610') - }) + cy.tick(10000); // 10 seconds passed + cy.get("#tick-div").click().should("have.text", "1489449610"); + }); - it('cy.stub() matches depending on arguments', () => { - // see all possible matchers at - // https://sinonjs.org/releases/latest/matchers/ - const greeter = { - /** - * Greets a person - * @param {string} name - */ - greet(name) { - return `Hello, ${name}!` - }, - } + it("cy.stub() matches depending on arguments", () => { + // see all possible matchers at + // https://sinonjs.org/releases/latest/matchers/ + const greeter = { + /** + * Greets a person + * @param {string} name + */ + greet(name) { + return `Hello, ${name}!`; + } + }; - cy.stub(greeter, 'greet') - .callThrough() // if you want non-matched calls to call the real method - .withArgs(Cypress.sinon.match.string).returns('Hi') - .withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name')) + cy.stub(greeter, "greet") + .callThrough() // if you want non-matched calls to call the real method + .withArgs(Cypress.sinon.match.string) + .returns("Hi") + .withArgs(Cypress.sinon.match.number) + .throws(new Error("Invalid name")); - expect(greeter.greet('World')).to.equal('Hi') - // @ts-ignore - expect(() => greeter.greet(42)).to.throw('Invalid name') - expect(greeter.greet).to.have.been.calledTwice + expect(greeter.greet("World")).to.equal("Hi"); + // @ts-ignore + expect(() => greeter.greet(42)).to.throw("Invalid name"); + expect(greeter.greet).to.have.been.calledTwice; - // non-matched calls goes the actual method - // @ts-ignore - expect(greeter.greet()).to.equal('Hello, undefined!') - }) + // non-matched calls goes the actual method + // @ts-ignore + expect(greeter.greet()).to.equal("Hello, undefined!"); + }); - it('matches call arguments using Sinon matchers', () => { - // see all possible matchers at - // https://sinonjs.org/releases/latest/matchers/ - const calculator = { - /** - * returns the sum of two arguments - * @param a {number} - * @param b {number} - */ - add(a, b) { - return a + b - }, - } + it("matches call arguments using Sinon matchers", () => { + // see all possible matchers at + // https://sinonjs.org/releases/latest/matchers/ + const calculator = { + /** + * returns the sum of two arguments + * @param a {number} + * @param b {number} + */ + add(a, b) { + return a + b; + } + }; - const spy = cy.spy(calculator, 'add').as('add') + const spy = cy.spy(calculator, "add").as("add"); - expect(calculator.add(2, 3)).to.equal(5) + expect(calculator.add(2, 3)).to.equal(5); - // if we want to assert the exact values used during the call - expect(spy).to.be.calledWith(2, 3) + // if we want to assert the exact values used during the call + expect(spy).to.be.calledWith(2, 3); - // let's confirm "add" method was called with two numbers - expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number) + // let's confirm "add" method was called with two numbers + expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number); - // alternatively, provide the value to match - expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)) + // alternatively, provide the value to match + expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)); - // match any value - expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3) + // match any value + expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3); - // match any value from a list - expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3) + // match any value from a list + expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3); - /** - * Returns true if the given number is event - * @param {number} x - */ - const isEven = (x) => x % 2 === 0 + /** + * Returns true if the given number is event + * @param {number} x + */ + const isEven = (x) => x % 2 === 0; - // expect the value to pass a custom predicate function - // the second argument to "sinon.match(predicate, message)" is - // shown if the predicate does not pass and assertion fails - expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3) + // expect the value to pass a custom predicate function + // the second argument to "sinon.match(predicate, message)" is + // shown if the predicate does not pass and assertion fails + expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3); - /** - * Returns a function that checks if a given number is larger than the limit - * @param {number} limit - * @returns {(x: number) => boolean} - */ - const isGreaterThan = (limit) => (x) => x > limit + /** + * Returns a function that checks if a given number is larger than the limit + * @param {number} limit + * @returns {(x: number) => boolean} + */ + const isGreaterThan = (limit) => (x) => x > limit; - /** - * Returns a function that checks if a given number is less than the limit - * @param {number} limit - * @returns {(x: number) => boolean} - */ - const isLessThan = (limit) => (x) => x < limit + /** + * Returns a function that checks if a given number is less than the limit + * @param {number} limit + * @returns {(x: number) => boolean} + */ + const isLessThan = (limit) => (x) => x < limit; - // you can combine several matchers using "and", "or" - expect(spy).to.be.calledWith( - Cypress.sinon.match.number, - Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')), - ) + // you can combine several matchers using "and", "or" + expect(spy).to.be.calledWith( + Cypress.sinon.match.number, + Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4")) + ); - expect(spy).to.be.calledWith( - Cypress.sinon.match.number, - Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)), - ) + expect(spy).to.be.calledWith( + Cypress.sinon.match.number, + Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3)) + ); - // matchers can be used from BDD assertions - cy.get('@add').should('have.been.calledWith', - Cypress.sinon.match.number, Cypress.sinon.match(3)) + // matchers can be used from BDD assertions + cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3)); - // you can alias matchers for shorter test code - const {match: M} = Cypress.sinon + // you can alias matchers for shorter test code + const { match: M } = Cypress.sinon; - cy.get('@add').should('have.been.calledWith', M.number, M(3)) - }) -}) + cy.get("@add").should("have.been.calledWith", M.number, M(3)); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/traversal.cy.js b/client/cypress/e2e/2-advanced-examples/traversal.cy.js index 35d8108d2..e6b56fde1 100644 --- a/client/cypress/e2e/2-advanced-examples/traversal.cy.js +++ b/client/cypress/e2e/2-advanced-examples/traversal.cy.js @@ -1,121 +1,97 @@ /// -context('Traversal', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/traversal') - }) +context("Traversal", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/traversal"); + }); - it('.children() - get child DOM elements', () => { - // https://on.cypress.io/children - cy.get('.traversal-breadcrumb') - .children('.active') - .should('contain', 'Data') - }) + it(".children() - get child DOM elements", () => { + // https://on.cypress.io/children + cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data"); + }); - it('.closest() - get closest ancestor DOM element', () => { - // https://on.cypress.io/closest - cy.get('.traversal-badge') - .closest('ul') - .should('have.class', 'list-group') - }) + it(".closest() - get closest ancestor DOM element", () => { + // https://on.cypress.io/closest + cy.get(".traversal-badge").closest("ul").should("have.class", "list-group"); + }); - it('.eq() - get a DOM element at a specific index', () => { - // https://on.cypress.io/eq - cy.get('.traversal-list>li') - .eq(1).should('contain', 'siamese') - }) + it(".eq() - get a DOM element at a specific index", () => { + // https://on.cypress.io/eq + cy.get(".traversal-list>li").eq(1).should("contain", "siamese"); + }); - it('.filter() - get DOM elements that match the selector', () => { - // https://on.cypress.io/filter - cy.get('.traversal-nav>li') - .filter('.active').should('contain', 'About') - }) + it(".filter() - get DOM elements that match the selector", () => { + // https://on.cypress.io/filter + cy.get(".traversal-nav>li").filter(".active").should("contain", "About"); + }); - it('.find() - get descendant DOM elements of the selector', () => { - // https://on.cypress.io/find - cy.get('.traversal-pagination') - .find('li').find('a') - .should('have.length', 7) - }) + it(".find() - get descendant DOM elements of the selector", () => { + // https://on.cypress.io/find + cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7); + }); - it('.first() - get first DOM element', () => { - // https://on.cypress.io/first - cy.get('.traversal-table td') - .first().should('contain', '1') - }) + it(".first() - get first DOM element", () => { + // https://on.cypress.io/first + cy.get(".traversal-table td").first().should("contain", "1"); + }); - it('.last() - get last DOM element', () => { - // https://on.cypress.io/last - cy.get('.traversal-buttons .btn') - .last().should('contain', 'Submit') - }) + it(".last() - get last DOM element", () => { + // https://on.cypress.io/last + cy.get(".traversal-buttons .btn").last().should("contain", "Submit"); + }); - it('.next() - get next sibling DOM element', () => { - // https://on.cypress.io/next - cy.get('.traversal-ul') - .contains('apples').next().should('contain', 'oranges') - }) + it(".next() - get next sibling DOM element", () => { + // https://on.cypress.io/next + cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges"); + }); - it('.nextAll() - get all next sibling DOM elements', () => { - // https://on.cypress.io/nextall - cy.get('.traversal-next-all') - .contains('oranges') - .nextAll().should('have.length', 3) - }) + it(".nextAll() - get all next sibling DOM elements", () => { + // https://on.cypress.io/nextall + cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3); + }); - it('.nextUntil() - get next sibling DOM elements until next el', () => { - // https://on.cypress.io/nextuntil - cy.get('#veggies') - .nextUntil('#nuts').should('have.length', 3) - }) + it(".nextUntil() - get next sibling DOM elements until next el", () => { + // https://on.cypress.io/nextuntil + cy.get("#veggies").nextUntil("#nuts").should("have.length", 3); + }); - it('.not() - remove DOM elements from set of DOM elements', () => { - // https://on.cypress.io/not - cy.get('.traversal-disabled .btn') - .not('[disabled]').should('not.contain', 'Disabled') - }) + it(".not() - remove DOM elements from set of DOM elements", () => { + // https://on.cypress.io/not + cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled"); + }); - it('.parent() - get parent DOM element from DOM elements', () => { - // https://on.cypress.io/parent - cy.get('.traversal-mark') - .parent().should('contain', 'Morbi leo risus') - }) + it(".parent() - get parent DOM element from DOM elements", () => { + // https://on.cypress.io/parent + cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus"); + }); - it('.parents() - get parent DOM elements from DOM elements', () => { - // https://on.cypress.io/parents - cy.get('.traversal-cite') - .parents().should('match', 'blockquote') - }) + it(".parents() - get parent DOM elements from DOM elements", () => { + // https://on.cypress.io/parents + cy.get(".traversal-cite").parents().should("match", "blockquote"); + }); - it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { - // https://on.cypress.io/parentsuntil - cy.get('.clothes-nav') - .find('.active') - .parentsUntil('.clothes-nav') - .should('have.length', 2) - }) + it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => { + // https://on.cypress.io/parentsuntil + cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2); + }); - it('.prev() - get previous sibling DOM element', () => { - // https://on.cypress.io/prev - cy.get('.birds').find('.active') - .prev().should('contain', 'Lorikeets') - }) + it(".prev() - get previous sibling DOM element", () => { + // https://on.cypress.io/prev + cy.get(".birds").find(".active").prev().should("contain", "Lorikeets"); + }); - it('.prevAll() - get all previous sibling DOM elements', () => { - // https://on.cypress.io/prevall - cy.get('.fruits-list').find('.third') - .prevAll().should('have.length', 2) - }) + it(".prevAll() - get all previous sibling DOM elements", () => { + // https://on.cypress.io/prevall + cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2); + }); - it('.prevUntil() - get all previous sibling DOM elements until el', () => { - // https://on.cypress.io/prevuntil - cy.get('.foods-list').find('#nuts') - .prevUntil('#veggies').should('have.length', 3) - }) + it(".prevUntil() - get all previous sibling DOM elements until el", () => { + // https://on.cypress.io/prevuntil + cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3); + }); - it('.siblings() - get all sibling DOM elements', () => { - // https://on.cypress.io/siblings - cy.get('.traversal-pills .active') - .siblings().should('have.length', 2) - }) -}) + it(".siblings() - get all sibling DOM elements", () => { + // https://on.cypress.io/siblings + cy.get(".traversal-pills .active").siblings().should("have.length", 2); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/utilities.cy.js b/client/cypress/e2e/2-advanced-examples/utilities.cy.js index d7d8d678f..c0c9e8196 100644 --- a/client/cypress/e2e/2-advanced-examples/utilities.cy.js +++ b/client/cypress/e2e/2-advanced-examples/utilities.cy.js @@ -1,110 +1,108 @@ /// -context('Utilities', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/utilities') - }) +context("Utilities", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/utilities"); + }); - it('Cypress._ - call a lodash method', () => { - // https://on.cypress.io/_ - cy.request('https://jsonplaceholder.cypress.io/users') - .then((response) => { - let ids = Cypress._.chain(response.body).map('id').take(3).value() + it("Cypress._ - call a lodash method", () => { + // https://on.cypress.io/_ + cy.request("https://jsonplaceholder.cypress.io/users").then((response) => { + let ids = Cypress._.chain(response.body).map("id").take(3).value(); - expect(ids).to.deep.eq([1, 2, 3]) - }) - }) + expect(ids).to.deep.eq([1, 2, 3]); + }); + }); - it('Cypress.$ - call a jQuery method', () => { - // https://on.cypress.io/$ - let $li = Cypress.$('.utility-jquery li:first') + it("Cypress.$ - call a jQuery method", () => { + // https://on.cypress.io/$ + let $li = Cypress.$(".utility-jquery li:first"); - cy.wrap($li) - .should('not.have.class', 'active') - .click() - .should('have.class', 'active') - }) + cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active"); + }); - it('Cypress.Blob - blob utilities and base64 string conversion', () => { - // https://on.cypress.io/blob - cy.get('.utility-blob').then(($div) => { - // https://github.com/nolanlawson/blob-util#imgSrcToDataURL - // get the dataUrl string for the javascript-logo - return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') - .then((dataUrl) => { - // create an element and set its src to the dataUrl - let img = Cypress.$('', {src: dataUrl}) + it("Cypress.Blob - blob utilities and base64 string conversion", () => { + // https://on.cypress.io/blob + cy.get(".utility-blob").then(($div) => { + // https://github.com/nolanlawson/blob-util#imgSrcToDataURL + // get the dataUrl string for the javascript-logo + return Cypress.Blob.imgSrcToDataURL( + "https://example.cypress.io/assets/img/javascript-logo.png", + undefined, + "anonymous" + ).then((dataUrl) => { + // create an element and set its src to the dataUrl + let img = Cypress.$("", { src: dataUrl }); - // need to explicitly return cy here since we are initially returning - // the Cypress.Blob.imgSrcToDataURL promise to our test - // append the image - $div.append(img) + // need to explicitly return cy here since we are initially returning + // the Cypress.Blob.imgSrcToDataURL promise to our test + // append the image + $div.append(img); - cy.get('.utility-blob img').click() - .should('have.attr', 'src', dataUrl) - }) - }) - }) + cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl); + }); + }); + }); - it('Cypress.minimatch - test out glob patterns against strings', () => { - // https://on.cypress.io/minimatch - let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { - matchBase: true, - }) + it("Cypress.minimatch - test out glob patterns against strings", () => { + // https://on.cypress.io/minimatch + let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", { + matchBase: true + }); - expect(matching, 'matching wildcard').to.be.true + expect(matching, "matching wildcard").to.be.true; - matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { - matchBase: true, - }) + matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", { + matchBase: true + }); - expect(matching, 'comments').to.be.false + expect(matching, "comments").to.be.false; - // ** matches against all downstream path segments - matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { - matchBase: true, - }) + // ** matches against all downstream path segments + matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", { + matchBase: true + }); - expect(matching, 'comments').to.be.true + expect(matching, "comments").to.be.true; - // whereas * matches only the next path segment + // whereas * matches only the next path segment - matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { - matchBase: false, - }) + matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", { + matchBase: false + }); - expect(matching, 'comments').to.be.false - }) + expect(matching, "comments").to.be.false; + }); - it('Cypress.Promise - instantiate a bluebird promise', () => { - // https://on.cypress.io/promise - let waited = false + it("Cypress.Promise - instantiate a bluebird promise", () => { + // https://on.cypress.io/promise + let waited = false; - /** - * @return Bluebird - */ - function waitOneSecond() { - // return a promise that resolves after 1 second - // @ts-ignore TS2351 (new Cypress.Promise) - return new Cypress.Promise((resolve, reject) => { - setTimeout(() => { - // set waited to true - waited = true + /** + * @return Bluebird + */ + function waitOneSecond() { + // return a promise that resolves after 1 second + // @ts-ignore TS2351 (new Cypress.Promise) + return new Cypress.Promise((resolve, reject) => { + setTimeout(() => { + // set waited to true + waited = true; - // resolve with 'foo' string - resolve('foo') - }, 1000) - }) - } + // resolve with 'foo' string + resolve("foo"); + }, 1000); + }); + } - cy.then(() => { - // return a promise to cy.then() that - // is awaited until it resolves - // @ts-ignore TS7006 - return waitOneSecond().then((str) => { - expect(str).to.eq('foo') - expect(waited).to.be.true - }) - }) - }) -}) + cy.then(() => { + // return a promise to cy.then() that + // is awaited until it resolves + // @ts-ignore TS7006 + return waitOneSecond().then((str) => { + expect(str).to.eq("foo"); + expect(waited).to.be.true; + }); + }); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/viewport.cy.js b/client/cypress/e2e/2-advanced-examples/viewport.cy.js index e640a1554..1d73b915d 100644 --- a/client/cypress/e2e/2-advanced-examples/viewport.cy.js +++ b/client/cypress/e2e/2-advanced-examples/viewport.cy.js @@ -1,59 +1,59 @@ /// -context('Viewport', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/viewport') - }) +context("Viewport", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/viewport"); + }); - it('cy.viewport() - set the viewport size and dimension', () => { - // https://on.cypress.io/viewport + it("cy.viewport() - set the viewport size and dimension", () => { + // https://on.cypress.io/viewport - cy.get('#navbar').should('be.visible') - cy.viewport(320, 480) + cy.get("#navbar").should("be.visible"); + cy.viewport(320, 480); - // the navbar should have collapse since our screen is smaller - cy.get('#navbar').should('not.be.visible') - cy.get('.navbar-toggle').should('be.visible').click() - cy.get('.nav').find('a').should('be.visible') + // the navbar should have collapse since our screen is smaller + cy.get("#navbar").should("not.be.visible"); + cy.get(".navbar-toggle").should("be.visible").click(); + cy.get(".nav").find("a").should("be.visible"); - // lets see what our app looks like on a super large screen - cy.viewport(2999, 2999) + // lets see what our app looks like on a super large screen + cy.viewport(2999, 2999); - // cy.viewport() accepts a set of preset sizes - // to easily set the screen to a device's width and height + // cy.viewport() accepts a set of preset sizes + // to easily set the screen to a device's width and height - // We added a cy.wait() between each viewport change so you can see - // the change otherwise it is a little too fast to see :) + // We added a cy.wait() between each viewport change so you can see + // the change otherwise it is a little too fast to see :) - cy.viewport('macbook-15') - cy.wait(200) - cy.viewport('macbook-13') - cy.wait(200) - cy.viewport('macbook-11') - cy.wait(200) - cy.viewport('ipad-2') - cy.wait(200) - cy.viewport('ipad-mini') - cy.wait(200) - cy.viewport('iphone-6+') - cy.wait(200) - cy.viewport('iphone-6') - cy.wait(200) - cy.viewport('iphone-5') - cy.wait(200) - cy.viewport('iphone-4') - cy.wait(200) - cy.viewport('iphone-3') - cy.wait(200) + cy.viewport("macbook-15"); + cy.wait(200); + cy.viewport("macbook-13"); + cy.wait(200); + cy.viewport("macbook-11"); + cy.wait(200); + cy.viewport("ipad-2"); + cy.wait(200); + cy.viewport("ipad-mini"); + cy.wait(200); + cy.viewport("iphone-6+"); + cy.wait(200); + cy.viewport("iphone-6"); + cy.wait(200); + cy.viewport("iphone-5"); + cy.wait(200); + cy.viewport("iphone-4"); + cy.wait(200); + cy.viewport("iphone-3"); + cy.wait(200); - // cy.viewport() accepts an orientation for all presets - // the default orientation is 'portrait' - cy.viewport('ipad-2', 'portrait') - cy.wait(200) - cy.viewport('iphone-4', 'landscape') - cy.wait(200) + // cy.viewport() accepts an orientation for all presets + // the default orientation is 'portrait' + cy.viewport("ipad-2", "portrait"); + cy.wait(200); + cy.viewport("iphone-4", "landscape"); + cy.wait(200); - // The viewport will be reset back to the default dimensions - // in between tests (the default can be set in cypress.json) - }) -}) + // The viewport will be reset back to the default dimensions + // in between tests (the default can be set in cypress.json) + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/waiting.cy.js b/client/cypress/e2e/2-advanced-examples/waiting.cy.js index b33f679d2..49915ae75 100644 --- a/client/cypress/e2e/2-advanced-examples/waiting.cy.js +++ b/client/cypress/e2e/2-advanced-examples/waiting.cy.js @@ -1,31 +1,31 @@ /// -context('Waiting', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/waiting') - }) - // BE CAREFUL of adding unnecessary wait times. - // https://on.cypress.io/best-practices#Unnecessary-Waiting +context("Waiting", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/waiting"); + }); + // BE CAREFUL of adding unnecessary wait times. + // https://on.cypress.io/best-practices#Unnecessary-Waiting - // https://on.cypress.io/wait - it('cy.wait() - wait for a specific amount of time', () => { - cy.get('.wait-input1').type('Wait 1000ms after typing') - cy.wait(1000) - cy.get('.wait-input2').type('Wait 1000ms after typing') - cy.wait(1000) - cy.get('.wait-input3').type('Wait 1000ms after typing') - cy.wait(1000) - }) + // https://on.cypress.io/wait + it("cy.wait() - wait for a specific amount of time", () => { + cy.get(".wait-input1").type("Wait 1000ms after typing"); + cy.wait(1000); + cy.get(".wait-input2").type("Wait 1000ms after typing"); + cy.wait(1000); + cy.get(".wait-input3").type("Wait 1000ms after typing"); + cy.wait(1000); + }); - it('cy.wait() - wait for a specific route', () => { - // Listen to GET to comments/1 - cy.intercept('GET', '**/comments/*').as('getComment') + it("cy.wait() - wait for a specific route", () => { + // Listen to GET to comments/1 + cy.intercept("GET", "**/comments/*").as("getComment"); - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.network-btn').click() + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get(".network-btn").click(); - // wait for GET comments/1 - cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) - }) -}) + // wait for GET comments/1 + cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]); + }); +}); diff --git a/client/cypress/e2e/2-advanced-examples/window.cy.js b/client/cypress/e2e/2-advanced-examples/window.cy.js index d8b3b237e..9740ba049 100644 --- a/client/cypress/e2e/2-advanced-examples/window.cy.js +++ b/client/cypress/e2e/2-advanced-examples/window.cy.js @@ -1,22 +1,22 @@ /// -context('Window', () => { - beforeEach(() => { - cy.visit('https://example.cypress.io/commands/window') - }) +context("Window", () => { + beforeEach(() => { + cy.visit("https://example.cypress.io/commands/window"); + }); - it('cy.window() - get the global window object', () => { - // https://on.cypress.io/window - cy.window().should('have.property', 'top') - }) + it("cy.window() - get the global window object", () => { + // https://on.cypress.io/window + cy.window().should("have.property", "top"); + }); - it('cy.document() - get the document object', () => { - // https://on.cypress.io/document - cy.document().should('have.property', 'charset').and('eq', 'UTF-8') - }) + it("cy.document() - get the document object", () => { + // https://on.cypress.io/document + cy.document().should("have.property", "charset").and("eq", "UTF-8"); + }); - it('cy.title() - get the title', () => { - // https://on.cypress.io/title - cy.title().should('include', 'Kitchen Sink') - }) -}) + it("cy.title() - get the title", () => { + // https://on.cypress.io/title + cy.title().should("include", "Kitchen Sink"); + }); +}); diff --git a/client/cypress/fixtures/profile.json b/client/cypress/fixtures/profile.json index b6c355ca5..a95e88f9c 100644 --- a/client/cypress/fixtures/profile.json +++ b/client/cypress/fixtures/profile.json @@ -2,4 +2,4 @@ "id": 8739, "name": "Jane", "email": "jane@example.com" -} \ No newline at end of file +} diff --git a/client/cypress/plugins/index.js b/client/cypress/plugins/index.js index fe51f29d4..8229063ad 100644 --- a/client/cypress/plugins/index.js +++ b/client/cypress/plugins/index.js @@ -17,6 +17,6 @@ */ // eslint-disable-next-line no-unused-vars module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/client/cypress/support/e2e.js b/client/cypress/support/e2e.js index d68db96df..d076cec9f 100644 --- a/client/cypress/support/e2e.js +++ b/client/cypress/support/e2e.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import "./commands"; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json index 8e6ac9683..36de33dee 100644 --- a/client/cypress/tsconfig.json +++ b/client/cypress/tsconfig.json @@ -2,11 +2,7 @@ "compilerOptions": { "allowJs": true, "baseUrl": "../node_modules", - "types": [ - "cypress" - ] + "types": ["cypress"] }, - "include": [ - "**/*.*" - ] + "include": ["**/*.*"] } diff --git a/client/dev-dist/registerSW.js b/client/dev-dist/registerSW.js index 1d5625f45..f17aa66d6 100644 --- a/client/dev-dist/registerSW.js +++ b/client/dev-dist/registerSW.js @@ -1 +1,2 @@ -if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' }) \ No newline at end of file +if ("serviceWorker" in navigator) + navigator.serviceWorker.register("/dev-sw.js?dev-sw", { scope: "/", type: "classic" }); diff --git a/client/dev-dist/sw.js b/client/dev-dist/sw.js index d14df90f7..0cc1553d3 100644 --- a/client/dev-dist/sw.js +++ b/client/dev-dist/sw.js @@ -21,22 +21,20 @@ if (!self.define) { const singleRequire = (uri, parentUri) => { uri = new URL(uri + ".js", parentUri).href; - return registry[uri] || ( - - new Promise(resolve => { - if ("document" in self) { - const script = document.createElement("script"); - script.src = uri; - script.onload = resolve; - document.head.appendChild(script); - } else { - nextDefineUri = uri; - importScripts(uri); - resolve(); - } - }) - - .then(() => { + return ( + registry[uri] || + new Promise((resolve) => { + if ("document" in self) { + const script = document.createElement("script"); + script.src = uri; + script.onload = resolve; + document.head.appendChild(script); + } else { + nextDefineUri = uri; + importScripts(uri); + resolve(); + } + }).then(() => { let promise = registry[uri]; if (!promise) { throw new Error(`Module ${uri} didn’t register its module`); @@ -53,21 +51,20 @@ if (!self.define) { return; } let exports = {}; - const require = depUri => singleRequire(depUri, uri); + const require = (depUri) => singleRequire(depUri, uri); const specialDeps = { module: { uri }, exports, require }; - registry[uri] = Promise.all(depsNames.map( - depName => specialDeps[depName] || require(depName) - )).then(deps => { + registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => { factory(...deps); return exports; }); }; } -define(['./workbox-b5f7729d'], (function (workbox) { 'use strict'; +define(["./workbox-b5f7729d"], function (workbox) { + "use strict"; self.skipWaiting(); workbox.clientsClaim(); @@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict'; * requests for URLs in the manifest. * See https://goo.gl/S9QRab */ - workbox.precacheAndRoute([{ - "url": "registerSW.js", - "revision": "3ca0b8505b4bec776b69afdba2768812" - }, { - "url": "index.html", - "revision": "0.sa702m4aq68" - }], {}); + workbox.precacheAndRoute( + [ + { + url: "registerSW.js", + revision: "3ca0b8505b4bec776b69afdba2768812" + }, + { + url: "index.html", + revision: "0.sa702m4aq68" + } + ], + {} + ); workbox.cleanupOutdatedCaches(); - workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { - allowlist: [/^\/$/] - })); - -})); + workbox.registerRoute( + new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { + allowlist: [/^\/$/] + }) + ); +}); diff --git a/client/dev-dist/workbox-b5f7729d.js b/client/dev-dist/workbox-b5f7729d.js index 077fa26dd..a0a2a61d8 100644 --- a/client/dev-dist/workbox-b5f7729d.js +++ b/client/dev-dist/workbox-b5f7729d.js @@ -1,782 +1,780 @@ -define(['exports'], (function (exports) { 'use strict'; +define(["exports"], function (exports) { + "use strict"; - // @ts-ignore - try { - self['workbox:core:7.0.0'] && _(); - } catch (e) {} + // @ts-ignore + try { + self["workbox:core:7.0.0"] && _(); + } catch (e) {} - /* + /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - /** - * Claim any currently available clients once the service worker - * becomes active. This is normally used in conjunction with `skipWaiting()`. - * - * @memberof workbox-core - */ - function clientsClaim() { - self.addEventListener('activate', () => self.clients.claim()); + /** + * Claim any currently available clients once the service worker + * becomes active. This is normally used in conjunction with `skipWaiting()`. + * + * @memberof workbox-core + */ + function clientsClaim() { + self.addEventListener("activate", () => self.clients.claim()); + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const logger = (() => { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!("__WB_DISABLE_DEV_LOGS" in globalThis)) { + self.__WB_DISABLE_DEV_LOGS = false; } - - /* - Copyright 2019 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const logger = (() => { - // Don't overwrite this value if it's already set. - // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 - if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) { - self.__WB_DISABLE_DEV_LOGS = false; + let inGroup = false; + const methodToColorMap = { + debug: `#7f8c8d`, + log: `#2ecc71`, + warn: `#f39c12`, + error: `#c0392b`, + groupCollapsed: `#3498db`, + groupEnd: null // No colored prefix on groupEnd + }; + const print = function (method, args) { + if (self.__WB_DISABLE_DEV_LOGS) { + return; } - let inGroup = false; - const methodToColorMap = { - debug: `#7f8c8d`, - log: `#2ecc71`, - warn: `#f39c12`, - error: `#c0392b`, - groupCollapsed: `#3498db`, - groupEnd: null // No colored prefix on groupEnd - }; - const print = function (method, args) { - if (self.__WB_DISABLE_DEV_LOGS) { + if (method === "groupCollapsed") { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + console[method](...args); return; } - if (method === 'groupCollapsed') { - // Safari doesn't print all console.groupCollapsed() arguments: - // https://bugs.webkit.org/show_bug.cgi?id=182754 - if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { - console[method](...args); - return; - } - } - const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; - // When in a group, the workbox prefix is not displayed. - const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; - console[method](...logPrefix, ...args); - if (method === 'groupCollapsed') { - inGroup = true; - } - if (method === 'groupEnd') { - inGroup = false; - } + } + const styles = [ + `background: ${methodToColorMap[method]}`, + `border-radius: 0.5em`, + `color: white`, + `font-weight: bold`, + `padding: 2px 0.5em` + ]; + // When in a group, the workbox prefix is not displayed. + const logPrefix = inGroup ? [] : ["%cworkbox", styles.join(";")]; + console[method](...logPrefix, ...args); + if (method === "groupCollapsed") { + inGroup = true; + } + if (method === "groupEnd") { + inGroup = false; + } + }; + // eslint-disable-next-line @typescript-eslint/ban-types + const api = {}; + const loggerMethods = Object.keys(methodToColorMap); + for (const key of loggerMethods) { + const method = key; + api[method] = (...args) => { + print(method, args); }; - // eslint-disable-next-line @typescript-eslint/ban-types - const api = {}; - const loggerMethods = Object.keys(methodToColorMap); - for (const key of loggerMethods) { - const method = key; - api[method] = (...args) => { - print(method, args); - }; - } - return api; - })(); - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const messages = { - 'invalid-value': ({ - paramName, - validValueDescription, - value - }) => { - if (!paramName || !validValueDescription) { - throw new Error(`Unexpected input to 'invalid-value' error.`); - } - return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; - }, - 'not-an-array': ({ - moduleName, - className, - funcName, - paramName - }) => { - if (!moduleName || !className || !funcName || !paramName) { - throw new Error(`Unexpected input to 'not-an-array' error.`); - } - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; - }, - 'incorrect-type': ({ - expectedType, - paramName, - moduleName, - className, - funcName - }) => { - if (!expectedType || !paramName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'incorrect-type' error.`); - } - const classNameStr = className ? `${className}.` : ''; - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`; - }, - 'incorrect-class': ({ - expectedClassName, - paramName, - moduleName, - className, - funcName, - isReturnValueProblem - }) => { - if (!expectedClassName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'incorrect-class' error.`); - } - const classNameStr = className ? `${className}.` : ''; - if (isReturnValueProblem) { - return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; - } - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; - }, - 'missing-a-method': ({ - expectedMethod, - paramName, - moduleName, - className, - funcName - }) => { - if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { - throw new Error(`Unexpected input to 'missing-a-method' error.`); - } - return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; - }, - 'add-to-cache-list-unexpected-type': ({ - entry - }) => { - return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; - }, - 'add-to-cache-list-conflicting-entries': ({ - firstEntry, - secondEntry - }) => { - if (!firstEntry || !secondEntry) { - throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); - } - return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; - }, - 'plugin-error-request-will-fetch': ({ - thrownErrorMessage - }) => { - if (!thrownErrorMessage) { - throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); - } - return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`; - }, - 'invalid-cache-name': ({ - cacheNameId, - value - }) => { - if (!cacheNameId) { - throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); - } - return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; - }, - 'unregister-route-but-not-found-with-method': ({ - method - }) => { - if (!method) { - throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); - } - return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; - }, - 'unregister-route-route-not-registered': () => { - return `The route you're trying to unregister was not previously ` + `registered.`; - }, - 'queue-replay-failed': ({ - name - }) => { - return `Replaying the background sync queue '${name}' failed.`; - }, - 'duplicate-queue-name': ({ - name - }) => { - return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; - }, - 'expired-test-without-max-age': ({ - methodName, - paramName - }) => { - return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; - }, - 'unsupported-route-type': ({ - moduleName, - className, - funcName, - paramName - }) => { - return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; - }, - 'not-array-of-class': ({ - value, - expectedClass, - moduleName, - className, - funcName, - paramName - }) => { - return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; - }, - 'max-entries-or-age-required': ({ - moduleName, - className, - funcName - }) => { - return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; - }, - 'statuses-or-headers-required': ({ - moduleName, - className, - funcName - }) => { - return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; - }, - 'invalid-string': ({ - moduleName, - funcName, - paramName - }) => { - if (!paramName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'invalid-string' error.`); - } - return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; - }, - 'channel-name-required': () => { - return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; - }, - 'invalid-responses-are-same-args': () => { - return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; - }, - 'expire-custom-caches-only': () => { - return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; - }, - 'unit-must-be-bytes': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); - } - return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; - }, - 'single-range-only': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'single-range-only' error.`); - } - return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; - }, - 'invalid-range-values': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'invalid-range-values' error.`); - } - return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; - }, - 'no-range-header': () => { - return `No Range header was found in the Request provided.`; - }, - 'range-not-satisfiable': ({ - size, - start, - end - }) => { - return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; - }, - 'attempt-to-cache-non-get-request': ({ - url, - method - }) => { - return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; - }, - 'cache-put-with-no-response': ({ - url - }) => { - return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; - }, - 'no-response': ({ - url, - error - }) => { - let message = `The strategy could not generate a response for '${url}'.`; - if (error) { - message += ` The underlying error is ${error}.`; - } - return message; - }, - 'bad-precaching-response': ({ - url, - status - }) => { - return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`); - }, - 'non-precached-url': ({ - url - }) => { - return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`; - }, - 'add-to-cache-list-conflicting-integrities': ({ - url - }) => { - return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`; - }, - 'missing-precache-entry': ({ - cacheName, - url - }) => { - return `Unable to find a precached response in ${cacheName} for ${url}.`; - }, - 'cross-origin-copy-response': ({ - origin - }) => { - return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`; - }, - 'opaque-streams-source': ({ - type - }) => { - const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`; - if (type === 'opaqueredirect') { - return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`; - } - return `${message} Please ensure your sources are CORS-enabled.`; - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const generatorFunction = (code, details = {}) => { - const message = messages[code]; - if (!message) { - throw new Error(`Unable to find message for code '${code}'.`); - } - return message(details); - }; - const messageGenerator = generatorFunction; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Workbox errors should be thrown with this class. - * This allows use to ensure the type easily in tests, - * helps developers identify errors from workbox - * easily and allows use to optimise error - * messages correctly. - * - * @private - */ - class WorkboxError extends Error { - /** - * - * @param {string} errorCode The error code that - * identifies this particular error. - * @param {Object=} details Any relevant arguments - * that will help developers identify issues should - * be added as a key on the context object. - */ - constructor(errorCode, details) { - const message = messageGenerator(errorCode, details); - super(message); - this.name = errorCode; - this.details = details; - } } + return api; + })(); - /* + /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - /* - * This method throws if the supplied value is not an array. - * The destructed values are required to produce a meaningful error for users. - * The destructed and restructured object is so it's clear what is - * needed. + const messages = { + "invalid-value": ({ paramName, validValueDescription, value }) => { + if (!paramName || !validValueDescription) { + throw new Error(`Unexpected input to 'invalid-value' error.`); + } + return ( + `The '${paramName}' parameter was given a value with an ` + + `unexpected value. ${validValueDescription} Received a value of ` + + `${JSON.stringify(value)}.` + ); + }, + "not-an-array": ({ moduleName, className, funcName, paramName }) => { + if (!moduleName || !className || !funcName || !paramName) { + throw new Error(`Unexpected input to 'not-an-array' error.`); + } + return ( + `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.` + ); + }, + "incorrect-type": ({ expectedType, paramName, moduleName, className, funcName }) => { + if (!expectedType || !paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-type' error.`); + } + const classNameStr = className ? `${className}.` : ""; + return ( + `The parameter '${paramName}' passed into ` + + `'${moduleName}.${classNameStr}` + + `${funcName}()' must be of type ${expectedType}.` + ); + }, + "incorrect-class": ({ expectedClassName, paramName, moduleName, className, funcName, isReturnValueProblem }) => { + if (!expectedClassName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-class' error.`); + } + const classNameStr = className ? `${className}.` : ""; + if (isReturnValueProblem) { + return ( + `The return value from ` + + `'${moduleName}.${classNameStr}${funcName}()' ` + + `must be an instance of class ${expectedClassName}.` + ); + } + return ( + `The parameter '${paramName}' passed into ` + + `'${moduleName}.${classNameStr}${funcName}()' ` + + `must be an instance of class ${expectedClassName}.` + ); + }, + "missing-a-method": ({ expectedMethod, paramName, moduleName, className, funcName }) => { + if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { + throw new Error(`Unexpected input to 'missing-a-method' error.`); + } + return ( + `${moduleName}.${className}.${funcName}() expected the ` + + `'${paramName}' parameter to expose a '${expectedMethod}' method.` + ); + }, + "add-to-cache-list-unexpected-type": ({ entry }) => { + return ( + `An unexpected entry was passed to ` + + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + + `strings with one or more characters, objects with a url property or ` + + `Request objects.` + ); + }, + "add-to-cache-list-conflicting-entries": ({ firstEntry, secondEntry }) => { + if (!firstEntry || !secondEntry) { + throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); + } + return ( + `Two of the entries passed to ` + + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + + `${firstEntry} but different revision details. Workbox is ` + + `unable to cache and version the asset correctly. Please remove one ` + + `of the entries.` + ); + }, + "plugin-error-request-will-fetch": ({ thrownErrorMessage }) => { + if (!thrownErrorMessage) { + throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); + } + return ( + `An error was thrown by a plugins 'requestWillFetch()' method. ` + + `The thrown error message was: '${thrownErrorMessage}'.` + ); + }, + "invalid-cache-name": ({ cacheNameId, value }) => { + if (!cacheNameId) { + throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); + } + return ( + `You must provide a name containing at least one character for ` + + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + + `'${JSON.stringify(value)}'` + ); + }, + "unregister-route-but-not-found-with-method": ({ method }) => { + if (!method) { + throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); + } + return ( + `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.` + ); + }, + "unregister-route-route-not-registered": () => { + return `The route you're trying to unregister was not previously ` + `registered.`; + }, + "queue-replay-failed": ({ name }) => { + return `Replaying the background sync queue '${name}' failed.`; + }, + "duplicate-queue-name": ({ name }) => { + return ( + `The Queue name '${name}' is already being used. ` + + `All instances of backgroundSync.Queue must be given unique names.` + ); + }, + "expired-test-without-max-age": ({ methodName, paramName }) => { + return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; + }, + "unsupported-route-type": ({ moduleName, className, funcName, paramName }) => { + return ( + `The supplied '${paramName}' parameter was an unsupported type. ` + + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + + `valid input types.` + ); + }, + "not-array-of-class": ({ value, expectedClass, moduleName, className, funcName, paramName }) => { + return ( + `The supplied '${paramName}' parameter must be an array of ` + + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + + `Please check the call to ${moduleName}.${className}.${funcName}() ` + + `to fix the issue.` + ); + }, + "max-entries-or-age-required": ({ moduleName, className, funcName }) => { + return ( + `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}` + ); + }, + "statuses-or-headers-required": ({ moduleName, className, funcName }) => { + return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; + }, + "invalid-string": ({ moduleName, funcName, paramName }) => { + if (!paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'invalid-string' error.`); + } + return ( + `When using strings, the '${paramName}' parameter must start with ` + + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + + `Please see the docs for ${moduleName}.${funcName}() for ` + + `more info.` + ); + }, + "channel-name-required": () => { + return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; + }, + "invalid-responses-are-same-args": () => { + return ( + `The arguments passed into responsesAreSame() appear to be ` + + `invalid. Please ensure valid Responses are used.` + ); + }, + "expire-custom-caches-only": () => { + return ( + `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.` + ); + }, + "unit-must-be-bytes": ({ normalizedRangeHeader }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); + } + return ( + `The 'unit' portion of the Range header must be set to 'bytes'. ` + + `The Range header provided was "${normalizedRangeHeader}"` + ); + }, + "single-range-only": ({ normalizedRangeHeader }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'single-range-only' error.`); + } + return ( + `Multiple ranges are not supported. Please use a single start ` + + `value, and optional end value. The Range header provided was ` + + `"${normalizedRangeHeader}"` + ); + }, + "invalid-range-values": ({ normalizedRangeHeader }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'invalid-range-values' error.`); + } + return ( + `The Range header is missing both start and end values. At least ` + + `one of those values is needed. The Range header provided was ` + + `"${normalizedRangeHeader}"` + ); + }, + "no-range-header": () => { + return `No Range header was found in the Request provided.`; + }, + "range-not-satisfiable": ({ size, start, end }) => { + return ( + `The start (${start}) and end (${end}) values in the Range are ` + + `not satisfiable by the cached response, which is ${size} bytes.` + ); + }, + "attempt-to-cache-non-get-request": ({ url, method }) => { + return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; + }, + "cache-put-with-no-response": ({ url }) => { + return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; + }, + "no-response": ({ url, error }) => { + let message = `The strategy could not generate a response for '${url}'.`; + if (error) { + message += ` The underlying error is ${error}.`; + } + return message; + }, + "bad-precaching-response": ({ url, status }) => { + return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`); + }, + "non-precached-url": ({ url }) => { + return ( + `createHandlerBoundToURL('${url}') was called, but that URL is ` + + `not precached. Please pass in a URL that is precached instead.` + ); + }, + "add-to-cache-list-conflicting-integrities": ({ url }) => { + return ( + `Two of the entries passed to ` + + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + + `${url} with different integrity values. Please remove one of them.` + ); + }, + "missing-precache-entry": ({ cacheName, url }) => { + return `Unable to find a precached response in ${cacheName} for ${url}.`; + }, + "cross-origin-copy-response": ({ origin }) => { + return ( + `workbox-core.copyResponse() can only be used with same-origin ` + + `responses. It was passed a response with origin ${origin}.` + ); + }, + "opaque-streams-source": ({ type }) => { + const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`; + if (type === "opaqueredirect") { + return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`; + } + return `${message} Please ensure your sources are CORS-enabled.`; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const generatorFunction = (code, details = {}) => { + const message = messages[code]; + if (!message) { + throw new Error(`Unable to find message for code '${code}'.`); + } + return message(details); + }; + const messageGenerator = generatorFunction; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Workbox errors should be thrown with this class. + * This allows use to ensure the type easily in tests, + * helps developers identify errors from workbox + * easily and allows use to optimise error + * messages correctly. + * + * @private + */ + class WorkboxError extends Error { + /** + * + * @param {string} errorCode The error code that + * identifies this particular error. + * @param {Object=} details Any relevant arguments + * that will help developers identify issues should + * be added as a key on the context object. */ - const isArray = (value, details) => { - if (!Array.isArray(value)) { - throw new WorkboxError('not-an-array', details); - } - }; - const hasMethod = (object, expectedMethod, details) => { - const type = typeof object[expectedMethod]; - if (type !== 'function') { - details['expectedMethod'] = expectedMethod; - throw new WorkboxError('missing-a-method', details); - } - }; - const isType = (object, expectedType, details) => { - if (typeof object !== expectedType) { - details['expectedType'] = expectedType; - throw new WorkboxError('incorrect-type', details); - } - }; - const isInstance = (object, + constructor(errorCode, details) { + const message = messageGenerator(errorCode, details); + super(message); + this.name = errorCode; + this.details = details; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /* + * This method throws if the supplied value is not an array. + * The destructed values are required to produce a meaningful error for users. + * The destructed and restructured object is so it's clear what is + * needed. + */ + const isArray = (value, details) => { + if (!Array.isArray(value)) { + throw new WorkboxError("not-an-array", details); + } + }; + const hasMethod = (object, expectedMethod, details) => { + const type = typeof object[expectedMethod]; + if (type !== "function") { + details["expectedMethod"] = expectedMethod; + throw new WorkboxError("missing-a-method", details); + } + }; + const isType = (object, expectedType, details) => { + if (typeof object !== expectedType) { + details["expectedType"] = expectedType; + throw new WorkboxError("incorrect-type", details); + } + }; + const isInstance = ( + object, // Need the general type to do the check later. // eslint-disable-next-line @typescript-eslint/ban-types - expectedClass, details) => { - if (!(object instanceof expectedClass)) { - details['expectedClassName'] = expectedClass.name; - throw new WorkboxError('incorrect-class', details); - } - }; - const isOneOf = (value, validValues, details) => { - if (!validValues.includes(value)) { - details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`; - throw new WorkboxError('invalid-value', details); - } - }; - const isArrayOfClass = (value, + expectedClass, + details + ) => { + if (!(object instanceof expectedClass)) { + details["expectedClassName"] = expectedClass.name; + throw new WorkboxError("incorrect-class", details); + } + }; + const isOneOf = (value, validValues, details) => { + if (!validValues.includes(value)) { + details["validValueDescription"] = `Valid values are ${JSON.stringify(validValues)}.`; + throw new WorkboxError("invalid-value", details); + } + }; + const isArrayOfClass = ( + value, // Need general type to do check later. expectedClass, // eslint-disable-line - details) => { - const error = new WorkboxError('not-array-of-class', details); - if (!Array.isArray(value)) { + details + ) => { + const error = new WorkboxError("not-array-of-class", details); + if (!Array.isArray(value)) { + throw error; + } + for (const item of value) { + if (!(item instanceof expectedClass)) { throw error; } - for (const item of value) { - if (!(item instanceof expectedClass)) { - throw error; - } - } - }; - const finalAssertExports = { - hasMethod, - isArray, - isInstance, - isOneOf, - isType, - isArrayOfClass - }; - - // @ts-ignore - try { - self['workbox:routing:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The default HTTP method, 'GET', used when there's no specific method - * configured for a route. - * - * @type {string} - * - * @private - */ - const defaultMethod = 'GET'; - /** - * The list of valid HTTP methods associated with requests that could be routed. - * - * @type {Array} - * - * @private - */ - const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * @param {function()|Object} handler Either a function, or an object with a - * 'handle' method. - * @return {Object} An object with a handle method. - * - * @private - */ - const normalizeHandler = handler => { - if (handler && typeof handler === 'object') { - { - finalAssertExports.hasMethod(handler, 'handle', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'handler' - }); - } - return handler; - } else { - { - finalAssertExports.isType(handler, 'function', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'handler' - }); - } - return { - handle: handler - }; - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * A `Route` consists of a pair of callback functions, "match" and "handler". - * The "match" callback determine if a route should be used to "handle" a - * request by returning a non-falsy value if it can. The "handler" callback - * is called when there is a match and should return a Promise that resolves - * to a `Response`. - * - * @memberof workbox-routing - */ - class Route { - /** - * Constructor for Route class. - * - * @param {workbox-routing~matchCallback} match - * A callback function that determines whether the route matches a given - * `fetch` event by returning a non-falsy value. - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resolving to a Response. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - */ - constructor(match, handler, method = defaultMethod) { - { - finalAssertExports.isType(match, 'function', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'match' - }); - if (method) { - finalAssertExports.isOneOf(method, validMethods, { - paramName: 'method' - }); - } - } - // These values are referenced directly by Router so cannot be - // altered by minificaton. - this.handler = normalizeHandler(handler); - this.match = match; - this.method = method; - } - /** - * - * @param {workbox-routing-handlerCallback} handler A callback - * function that returns a Promise resolving to a Response - */ - setCatchHandler(handler) { - this.catchHandler = normalizeHandler(handler); - } } + }; + const finalAssertExports = { + hasMethod, + isArray, + isInstance, + isOneOf, + isType, + isArrayOfClass + }; - /* + // @ts-ignore + try { + self["workbox:routing:7.0.0"] && _(); + } catch (e) {} + + /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - /** - * RegExpRoute makes it easy to create a regular expression based - * {@link workbox-routing.Route}. - * - * For same-origin requests the RegExp only needs to match part of the URL. For - * requests against third-party servers, you must define a RegExp that matches - * the start of the URL. - * - * @memberof workbox-routing - * @extends workbox-routing.Route - */ - class RegExpRoute extends Route { - /** - * If the regular expression contains - * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, - * the captured values will be passed to the - * {@link workbox-routing~handlerCallback} `params` - * argument. - * - * @param {RegExp} regExp The regular expression to match against URLs. - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - */ - constructor(regExp, handler, method) { - { - finalAssertExports.isInstance(regExp, RegExp, { - moduleName: 'workbox-routing', - className: 'RegExpRoute', - funcName: 'constructor', - paramName: 'pattern' - }); - } - const match = ({ - url - }) => { - const result = regExp.exec(url.href); - // Return immediately if there's no match. - if (!result) { - return; - } - // Require that the match start at the first character in the URL string - // if it's a cross-origin request. - // See https://github.com/GoogleChrome/workbox/issues/281 for the context - // behind this behavior. - if (url.origin !== location.origin && result.index !== 0) { - { - logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); - } - return; - } - // If the route matches, but there aren't any capture groups defined, then - // this will return [], which is truthy and therefore sufficient to - // indicate a match. - // If there are capture groups, then it will return their values. - return result.slice(1); - }; - super(match, handler, method); - } - } + /** + * The default HTTP method, 'GET', used when there's no specific method + * configured for a route. + * + * @type {string} + * + * @private + */ + const defaultMethod = "GET"; + /** + * The list of valid HTTP methods associated with requests that could be routed. + * + * @type {Array} + * + * @private + */ + const validMethods = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"]; - /* + /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - const getFriendlyURL = url => { - const urlObj = new URL(String(url), location.href); - // See https://github.com/GoogleChrome/workbox/issues/2323 - // We want to include everything, except for the origin if it's same-origin. - return urlObj.href.replace(new RegExp(`^${location.origin}`), ''); - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The Router can be used to process a `FetchEvent` using one or more - * {@link workbox-routing.Route}, responding with a `Response` if - * a matching route exists. - * - * If no route matches a given a request, the Router will use a "default" - * handler if one is defined. - * - * Should the matching Route throw an error, the Router will use a "catch" - * handler if one is defined to gracefully deal with issues and respond with a - * Request. - * - * If a request matches multiple routes, the **earliest** registered route will - * be used to respond to the request. - * - * @memberof workbox-routing - */ - class Router { - /** - * Initializes a new Router. - */ - constructor() { - this._routes = new Map(); - this._defaultHandlerMap = new Map(); - } - /** - * @return {Map>} routes A `Map` of HTTP - * method name ('GET', etc.) to an array of all the corresponding `Route` - * instances that are registered. - */ - get routes() { - return this._routes; - } - /** - * Adds a fetch event listener to respond to events when a route matches - * the event's request. - */ - addFetchListener() { - // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 - self.addEventListener('fetch', event => { - const { - request - } = event; - const responsePromise = this.handleRequest({ - request, - event - }); - if (responsePromise) { - event.respondWith(responsePromise); - } + /** + * @param {function()|Object} handler Either a function, or an object with a + * 'handle' method. + * @return {Object} An object with a handle method. + * + * @private + */ + const normalizeHandler = (handler) => { + if (handler && typeof handler === "object") { + { + finalAssertExports.hasMethod(handler, "handle", { + moduleName: "workbox-routing", + className: "Route", + funcName: "constructor", + paramName: "handler" }); } - /** - * Adds a message event listener for URLs to cache from the window. - * This is useful to cache resources loaded on the page prior to when the - * service worker started controlling it. - * - * The format of the message data sent from the window should be as follows. - * Where the `urlsToCache` array may consist of URL strings or an array of - * URL string + `requestInit` object (the same as you'd pass to `fetch()`). - * - * ``` - * { - * type: 'CACHE_URLS', - * payload: { - * urlsToCache: [ - * './script1.js', - * './script2.js', - * ['./script3.js', {mode: 'no-cors'}], - * ], - * }, - * } - * ``` - */ - addCacheListener() { - // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 - self.addEventListener('message', event => { - // event.data is type 'any' - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (event.data && event.data.type === 'CACHE_URLS') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { - payload - } = event.data; - { - logger.debug(`Caching URLs from the window`, payload.urlsToCache); - } - const requestPromises = Promise.all(payload.urlsToCache.map(entry => { - if (typeof entry === 'string') { + return handler; + } else { + { + finalAssertExports.isType(handler, "function", { + moduleName: "workbox-routing", + className: "Route", + funcName: "constructor", + paramName: "handler" + }); + } + return { + handle: handler + }; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A `Route` consists of a pair of callback functions, "match" and "handler". + * The "match" callback determine if a route should be used to "handle" a + * request by returning a non-falsy value if it can. The "handler" callback + * is called when there is a match and should return a Promise that resolves + * to a `Response`. + * + * @memberof workbox-routing + */ + class Route { + /** + * Constructor for Route class. + * + * @param {workbox-routing~matchCallback} match + * A callback function that determines whether the route matches a given + * `fetch` event by returning a non-falsy value. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resolving to a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(match, handler, method = defaultMethod) { + { + finalAssertExports.isType(match, "function", { + moduleName: "workbox-routing", + className: "Route", + funcName: "constructor", + paramName: "match" + }); + if (method) { + finalAssertExports.isOneOf(method, validMethods, { + paramName: "method" + }); + } + } + // These values are referenced directly by Router so cannot be + // altered by minificaton. + this.handler = normalizeHandler(handler); + this.match = match; + this.method = method; + } + /** + * + * @param {workbox-routing-handlerCallback} handler A callback + * function that returns a Promise resolving to a Response + */ + setCatchHandler(handler) { + this.catchHandler = normalizeHandler(handler); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * RegExpRoute makes it easy to create a regular expression based + * {@link workbox-routing.Route}. + * + * For same-origin requests the RegExp only needs to match part of the URL. For + * requests against third-party servers, you must define a RegExp that matches + * the start of the URL. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class RegExpRoute extends Route { + /** + * If the regular expression contains + * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, + * the captured values will be passed to the + * {@link workbox-routing~handlerCallback} `params` + * argument. + * + * @param {RegExp} regExp The regular expression to match against URLs. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(regExp, handler, method) { + { + finalAssertExports.isInstance(regExp, RegExp, { + moduleName: "workbox-routing", + className: "RegExpRoute", + funcName: "constructor", + paramName: "pattern" + }); + } + const match = ({ url }) => { + const result = regExp.exec(url.href); + // Return immediately if there's no match. + if (!result) { + return; + } + // Require that the match start at the first character in the URL string + // if it's a cross-origin request. + // See https://github.com/GoogleChrome/workbox/issues/281 for the context + // behind this behavior. + if (url.origin !== location.origin && result.index !== 0) { + { + logger.debug( + `The regular expression '${regExp.toString()}' only partially matched ` + + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + + `handle cross-origin requests if they match the entire URL.` + ); + } + return; + } + // If the route matches, but there aren't any capture groups defined, then + // this will return [], which is truthy and therefore sufficient to + // indicate a match. + // If there are capture groups, then it will return their values. + return result.slice(1); + }; + super(match, handler, method); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const getFriendlyURL = (url) => { + const urlObj = new URL(String(url), location.href); + // See https://github.com/GoogleChrome/workbox/issues/2323 + // We want to include everything, except for the origin if it's same-origin. + return urlObj.href.replace(new RegExp(`^${location.origin}`), ""); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Router can be used to process a `FetchEvent` using one or more + * {@link workbox-routing.Route}, responding with a `Response` if + * a matching route exists. + * + * If no route matches a given a request, the Router will use a "default" + * handler if one is defined. + * + * Should the matching Route throw an error, the Router will use a "catch" + * handler if one is defined to gracefully deal with issues and respond with a + * Request. + * + * If a request matches multiple routes, the **earliest** registered route will + * be used to respond to the request. + * + * @memberof workbox-routing + */ + class Router { + /** + * Initializes a new Router. + */ + constructor() { + this._routes = new Map(); + this._defaultHandlerMap = new Map(); + } + /** + * @return {Map>} routes A `Map` of HTTP + * method name ('GET', etc.) to an array of all the corresponding `Route` + * instances that are registered. + */ + get routes() { + return this._routes; + } + /** + * Adds a fetch event listener to respond to events when a route matches + * the event's request. + */ + addFetchListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener("fetch", (event) => { + const { request } = event; + const responsePromise = this.handleRequest({ + request, + event + }); + if (responsePromise) { + event.respondWith(responsePromise); + } + }); + } + /** + * Adds a message event listener for URLs to cache from the window. + * This is useful to cache resources loaded on the page prior to when the + * service worker started controlling it. + * + * The format of the message data sent from the window should be as follows. + * Where the `urlsToCache` array may consist of URL strings or an array of + * URL string + `requestInit` object (the same as you'd pass to `fetch()`). + * + * ``` + * { + * type: 'CACHE_URLS', + * payload: { + * urlsToCache: [ + * './script1.js', + * './script2.js', + * ['./script3.js', {mode: 'no-cors'}], + * ], + * }, + * } + * ``` + */ + addCacheListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener("message", (event) => { + // event.data is type 'any' + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (event.data && event.data.type === "CACHE_URLS") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { payload } = event.data; + { + logger.debug(`Caching URLs from the window`, payload.urlsToCache); + } + const requestPromises = Promise.all( + payload.urlsToCache.map((entry) => { + if (typeof entry === "string") { entry = [entry]; } const request = new Request(...entry); @@ -787,2351 +785,1955 @@ define(['exports'], (function (exports) { 'use strict'; // TODO(philipwalton): TypeScript errors without this typecast for // some reason (probably a bug). The real type here should work but // doesn't: `Array | undefined>`. - })); // TypeScript - event.waitUntil(requestPromises); - // If a MessageChannel was used, reply to the message on success. - if (event.ports && event.ports[0]) { - void requestPromises.then(() => event.ports[0].postMessage(true)); - } + }) + ); // TypeScript + event.waitUntil(requestPromises); + // If a MessageChannel was used, reply to the message on success. + if (event.ports && event.ports[0]) { + void requestPromises.then(() => event.ports[0].postMessage(true)); } + } + }); + } + /** + * Apply the routing rules to a FetchEvent object to get a Response from an + * appropriate Route's handler. + * + * @param {Object} options + * @param {Request} options.request The request to handle. + * @param {ExtendableEvent} options.event The event that triggered the + * request. + * @return {Promise|undefined} A promise is returned if a + * registered route can handle the request. If there is no matching + * route and there's no `defaultHandler`, `undefined` is returned. + */ + handleRequest({ request, event }) { + { + finalAssertExports.isInstance(request, Request, { + moduleName: "workbox-routing", + className: "Router", + funcName: "handleRequest", + paramName: "options.request" }); } - /** - * Apply the routing rules to a FetchEvent object to get a Response from an - * appropriate Route's handler. - * - * @param {Object} options - * @param {Request} options.request The request to handle. - * @param {ExtendableEvent} options.event The event that triggered the - * request. - * @return {Promise|undefined} A promise is returned if a - * registered route can handle the request. If there is no matching - * route and there's no `defaultHandler`, `undefined` is returned. - */ - handleRequest({ - request, - event - }) { + const url = new URL(request.url, location.href); + if (!url.protocol.startsWith("http")) { { - finalAssertExports.isInstance(request, Request, { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'handleRequest', - paramName: 'options.request' - }); + logger.debug(`Workbox Router only supports URLs that start with 'http'.`); } - const url = new URL(request.url, location.href); - if (!url.protocol.startsWith('http')) { - { - logger.debug(`Workbox Router only supports URLs that start with 'http'.`); - } - return; - } - const sameOrigin = url.origin === location.origin; - const { - params, - route - } = this.findMatchingRoute({ - event, - request, - sameOrigin, - url - }); - let handler = route && route.handler; - const debugMessages = []; - { - if (handler) { - debugMessages.push([`Found a route to handle this request:`, route]); - if (params) { - debugMessages.push([`Passing the following params to the route's handler:`, params]); - } - } - } - // If we don't have a handler because there was no matching route, then - // fall back to defaultHandler if that's defined. - const method = request.method; - if (!handler && this._defaultHandlerMap.has(method)) { - { - debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`); - } - handler = this._defaultHandlerMap.get(method); - } - if (!handler) { - { - // No handler so Workbox will do nothing. If logs is set of debug - // i.e. verbose, we should print out this information. - logger.debug(`No route found for: ${getFriendlyURL(url)}`); - } - return; - } - { - // We have a handler, meaning Workbox is going to handle the route. - // print the routing details to the console. - logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); - debugMessages.forEach(msg => { - if (Array.isArray(msg)) { - logger.log(...msg); - } else { - logger.log(msg); - } - }); - logger.groupEnd(); - } - // Wrap in try and catch in case the handle method throws a synchronous - // error. It should still callback to the catch handler. - let responsePromise; - try { - responsePromise = handler.handle({ - url, - request, - event, - params - }); - } catch (err) { - responsePromise = Promise.reject(err); - } - // Get route's catch handler, if it exists - const catchHandler = route && route.catchHandler; - if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { - responsePromise = responsePromise.catch(async err => { - // If there's a route catch handler, process that first - if (catchHandler) { - { - // Still include URL here as it will be async from the console group - // and may not make sense without the URL - logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); - logger.error(`Error thrown by:`, route); - logger.error(err); - logger.groupEnd(); - } - try { - return await catchHandler.handle({ - url, - request, - event, - params - }); - } catch (catchErr) { - if (catchErr instanceof Error) { - err = catchErr; - } - } - } - if (this._catchHandler) { - { - // Still include URL here as it will be async from the console group - // and may not make sense without the URL - logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); - logger.error(`Error thrown by:`, route); - logger.error(err); - logger.groupEnd(); - } - return this._catchHandler.handle({ - url, - request, - event - }); - } - throw err; - }); - } - return responsePromise; - } - /** - * Checks a request and URL (and optionally an event) against the list of - * registered routes, and if there's a match, returns the corresponding - * route along with any params generated by the match. - * - * @param {Object} options - * @param {URL} options.url - * @param {boolean} options.sameOrigin The result of comparing `url.origin` - * against the current origin. - * @param {Request} options.request The request to match. - * @param {Event} options.event The corresponding event. - * @return {Object} An object with `route` and `params` properties. - * They are populated if a matching route was found or `undefined` - * otherwise. - */ - findMatchingRoute({ - url, - sameOrigin, - request, - event - }) { - const routes = this._routes.get(request.method) || []; - for (const route of routes) { - let params; - // route.match returns type any, not possible to change right now. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const matchResult = route.match({ - url, - sameOrigin, - request, - event - }); - if (matchResult) { - { - // Warn developers that using an async matchCallback is almost always - // not the right thing to do. - if (matchResult instanceof Promise) { - logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route); - } - } - // See https://github.com/GoogleChrome/workbox/issues/2079 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - params = matchResult; - if (Array.isArray(params) && params.length === 0) { - // Instead of passing an empty array in as params, use undefined. - params = undefined; - } else if (matchResult.constructor === Object && - // eslint-disable-line - Object.keys(matchResult).length === 0) { - // Instead of passing an empty object in as params, use undefined. - params = undefined; - } else if (typeof matchResult === 'boolean') { - // For the boolean value true (rather than just something truth-y), - // don't set params. - // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 - params = undefined; - } - // Return early if have a match. - return { - route, - params - }; - } - } - // If no match was found above, return and empty object. - return {}; - } - /** - * Define a default `handler` that's called when no routes explicitly - * match the incoming request. - * - * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. - * - * Without a default handler, unmatched requests will go against the - * network as if there were no service worker present. - * - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - * @param {string} [method='GET'] The HTTP method to associate with this - * default handler. Each method has its own default. - */ - setDefaultHandler(handler, method = defaultMethod) { - this._defaultHandlerMap.set(method, normalizeHandler(handler)); - } - /** - * If a Route throws an error while handling a request, this `handler` - * will be called and given a chance to provide a response. - * - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - */ - setCatchHandler(handler) { - this._catchHandler = normalizeHandler(handler); - } - /** - * Registers a route with the router. - * - * @param {workbox-routing.Route} route The route to register. - */ - registerRoute(route) { - { - finalAssertExports.isType(route, 'object', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.hasMethod(route, 'match', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.isType(route.handler, 'object', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.hasMethod(route.handler, 'handle', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route.handler' - }); - finalAssertExports.isType(route.method, 'string', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route.method' - }); - } - if (!this._routes.has(route.method)) { - this._routes.set(route.method, []); - } - // Give precedence to all of the earlier routes by adding this additional - // route to the end of the array. - this._routes.get(route.method).push(route); - } - /** - * Unregisters a route with the router. - * - * @param {workbox-routing.Route} route The route to unregister. - */ - unregisterRoute(route) { - if (!this._routes.has(route.method)) { - throw new WorkboxError('unregister-route-but-not-found-with-method', { - method: route.method - }); - } - const routeIndex = this._routes.get(route.method).indexOf(route); - if (routeIndex > -1) { - this._routes.get(route.method).splice(routeIndex, 1); - } else { - throw new WorkboxError('unregister-route-route-not-registered'); - } - } - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - let defaultRouter; - /** - * Creates a new, singleton Router instance if one does not exist. If one - * does already exist, that instance is returned. - * - * @private - * @return {Router} - */ - const getOrCreateDefaultRouter = () => { - if (!defaultRouter) { - defaultRouter = new Router(); - // The helpers that use the default Router assume these listeners exist. - defaultRouter.addFetchListener(); - defaultRouter.addCacheListener(); - } - return defaultRouter; - }; - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Easily register a RegExp, string, or function with a caching - * strategy to a singleton Router instance. - * - * This method will generate a Route for you if needed and - * call {@link workbox-routing.Router#registerRoute}. - * - * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture - * If the capture param is a `Route`, all other arguments will be ignored. - * @param {workbox-routing~handlerCallback} [handler] A callback - * function that returns a Promise resulting in a Response. This parameter - * is required if `capture` is not a `Route` object. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - * @return {workbox-routing.Route} The generated `Route`. - * - * @memberof workbox-routing - */ - function registerRoute(capture, handler, method) { - let route; - if (typeof capture === 'string') { - const captureUrl = new URL(capture, location.href); - { - if (!(capture.startsWith('/') || capture.startsWith('http'))) { - throw new WorkboxError('invalid-string', { - moduleName: 'workbox-routing', - funcName: 'registerRoute', - paramName: 'capture' - }); - } - // We want to check if Express-style wildcards are in the pathname only. - // TODO: Remove this log message in v4. - const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; - // See https://github.com/pillarjs/path-to-regexp#parameters - const wildcards = '[*:?+]'; - if (new RegExp(`${wildcards}`).exec(valueToCheck)) { - logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); - } - } - const matchCallback = ({ - url - }) => { - { - if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { - logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); - } - } - return url.href === captureUrl.href; - }; - // If `capture` is a string then `handler` and `method` must be present. - route = new Route(matchCallback, handler, method); - } else if (capture instanceof RegExp) { - // If `capture` is a `RegExp` then `handler` and `method` must be present. - route = new RegExpRoute(capture, handler, method); - } else if (typeof capture === 'function') { - // If `capture` is a function then `handler` and `method` must be present. - route = new Route(capture, handler, method); - } else if (capture instanceof Route) { - route = capture; - } else { - throw new WorkboxError('unsupported-route-type', { - moduleName: 'workbox-routing', - funcName: 'registerRoute', - paramName: 'capture' - }); - } - const defaultRouter = getOrCreateDefaultRouter(); - defaultRouter.registerRoute(route); - return route; - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const _cacheNameDetails = { - googleAnalytics: 'googleAnalytics', - precache: 'precache-v2', - prefix: 'workbox', - runtime: 'runtime', - suffix: typeof registration !== 'undefined' ? registration.scope : '' - }; - const _createCacheName = cacheName => { - return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-'); - }; - const eachCacheNameDetail = fn => { - for (const key of Object.keys(_cacheNameDetails)) { - fn(key); - } - }; - const cacheNames = { - updateDetails: details => { - eachCacheNameDetail(key => { - if (typeof details[key] === 'string') { - _cacheNameDetails[key] = details[key]; - } - }); - }, - getGoogleAnalyticsName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); - }, - getPrecacheName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.precache); - }, - getPrefix: () => { - return _cacheNameDetails.prefix; - }, - getRuntimeName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.runtime); - }, - getSuffix: () => { - return _cacheNameDetails.suffix; - } - }; - - /* - Copyright 2020 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * A utility method that makes it easier to use `event.waitUntil` with - * async functions and return the result. - * - * @param {ExtendableEvent} event - * @param {Function} asyncFn - * @return {Function} - * @private - */ - function waitUntil(event, asyncFn) { - const returnPromise = asyncFn(); - event.waitUntil(returnPromise); - return returnPromise; - } - - // @ts-ignore - try { - self['workbox:precaching:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - // Name of the search parameter used to store revision info. - const REVISION_SEARCH_PARAM = '__WB_REVISION__'; - /** - * Converts a manifest entry into a versioned URL suitable for precaching. - * - * @param {Object|string} entry - * @return {string} A URL with versioning info. - * - * @private - * @memberof workbox-precaching - */ - function createCacheKey(entry) { - if (!entry) { - throw new WorkboxError('add-to-cache-list-unexpected-type', { - entry - }); - } - // If a precache manifest entry is a string, it's assumed to be a versioned - // URL, like '/app.abcd1234.js'. Return as-is. - if (typeof entry === 'string') { - const urlObject = new URL(entry, location.href); - return { - cacheKey: urlObject.href, - url: urlObject.href - }; - } - const { - revision, - url - } = entry; - if (!url) { - throw new WorkboxError('add-to-cache-list-unexpected-type', { - entry - }); - } - // If there's just a URL and no revision, then it's also assumed to be a - // versioned URL. - if (!revision) { - const urlObject = new URL(url, location.href); - return { - cacheKey: urlObject.href, - url: urlObject.href - }; - } - // Otherwise, construct a properly versioned URL using the custom Workbox - // search parameter along with the revision info. - const cacheKeyURL = new URL(url, location.href); - const originalURL = new URL(url, location.href); - cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); - return { - cacheKey: cacheKeyURL.href, - url: originalURL.href - }; - } - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * A plugin, designed to be used with PrecacheController, to determine the - * of assets that were updated (or not updated) during the install event. - * - * @private - */ - class PrecacheInstallReportPlugin { - constructor() { - this.updatedURLs = []; - this.notUpdatedURLs = []; - this.handlerWillStart = async ({ - request, - state - }) => { - // TODO: `state` should never be undefined... - if (state) { - state.originalRequest = request; - } - }; - this.cachedResponseWillBeUsed = async ({ - event, - state, - cachedResponse - }) => { - if (event.type === 'install') { - if (state && state.originalRequest && state.originalRequest instanceof Request) { - // TODO: `state` should never be undefined... - const url = state.originalRequest.url; - if (cachedResponse) { - this.notUpdatedURLs.push(url); - } else { - this.updatedURLs.push(url); - } - } - } - return cachedResponse; - }; - } - } - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * A plugin, designed to be used with PrecacheController, to translate URLs into - * the corresponding cache key, based on the current revision info. - * - * @private - */ - class PrecacheCacheKeyPlugin { - constructor({ - precacheController - }) { - this.cacheKeyWillBeUsed = async ({ - request, - params - }) => { - // Params is type any, can't change right now. - /* eslint-disable */ - const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) || this._precacheController.getCacheKeyForURL(request.url); - /* eslint-enable */ - return cacheKey ? new Request(cacheKey, { - headers: request.headers - }) : request; - }; - this._precacheController = precacheController; - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * @param {string} groupTitle - * @param {Array} deletedURLs - * - * @private - */ - const logGroup = (groupTitle, deletedURLs) => { - logger.groupCollapsed(groupTitle); - for (const url of deletedURLs) { - logger.log(url); - } - logger.groupEnd(); - }; - /** - * @param {Array} deletedURLs - * - * @private - * @memberof workbox-precaching - */ - function printCleanupDetails(deletedURLs) { - const deletionCount = deletedURLs.length; - if (deletionCount > 0) { - logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`); - logGroup('Deleted Cache Requests', deletedURLs); - logger.groupEnd(); - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * @param {string} groupTitle - * @param {Array} urls - * - * @private - */ - function _nestedGroup(groupTitle, urls) { - if (urls.length === 0) { return; } - logger.groupCollapsed(groupTitle); - for (const url of urls) { - logger.log(url); - } - logger.groupEnd(); - } - /** - * @param {Array} urlsToPrecache - * @param {Array} urlsAlreadyPrecached - * - * @private - * @memberof workbox-precaching - */ - function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { - const precachedCount = urlsToPrecache.length; - const alreadyPrecachedCount = urlsAlreadyPrecached.length; - if (precachedCount || alreadyPrecachedCount) { - let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`; - if (alreadyPrecachedCount > 0) { - message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`; + const sameOrigin = url.origin === location.origin; + const { params, route } = this.findMatchingRoute({ + event, + request, + sameOrigin, + url + }); + let handler = route && route.handler; + const debugMessages = []; + { + if (handler) { + debugMessages.push([`Found a route to handle this request:`, route]); + if (params) { + debugMessages.push([`Passing the following params to the route's handler:`, params]); + } } - logger.groupCollapsed(message); - _nestedGroup(`View newly precached URLs.`, urlsToPrecache); - _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); + } + // If we don't have a handler because there was no matching route, then + // fall back to defaultHandler if that's defined. + const method = request.method; + if (!handler && this._defaultHandlerMap.has(method)) { + { + debugMessages.push( + `Failed to find a matching route. Falling ` + `back to the default handler for ${method}.` + ); + } + handler = this._defaultHandlerMap.get(method); + } + if (!handler) { + { + // No handler so Workbox will do nothing. If logs is set of debug + // i.e. verbose, we should print out this information. + logger.debug(`No route found for: ${getFriendlyURL(url)}`); + } + return; + } + { + // We have a handler, meaning Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); + debugMessages.forEach((msg) => { + if (Array.isArray(msg)) { + logger.log(...msg); + } else { + logger.log(msg); + } + }); logger.groupEnd(); } - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - let supportStatus; - /** - * A utility function that determines whether the current browser supports - * constructing a new `Response` from a `response.body` stream. - * - * @return {boolean} `true`, if the current browser can successfully - * construct a `Response` from a `response.body` stream, `false` otherwise. - * - * @private - */ - function canConstructResponseFromBodyStream() { - if (supportStatus === undefined) { - const testResponse = new Response(''); - if ('body' in testResponse) { - try { - new Response(testResponse.body); - supportStatus = true; - } catch (error) { - supportStatus = false; - } - } - supportStatus = false; - } - return supportStatus; - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Allows developers to copy a response and modify its `headers`, `status`, - * or `statusText` values (the values settable via a - * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax} - * object in the constructor). - * To modify these values, pass a function as the second argument. That - * function will be invoked with a single object with the response properties - * `{headers, status, statusText}`. The return value of this function will - * be used as the `ResponseInit` for the new `Response`. To change the values - * either modify the passed parameter(s) and return it, or return a totally - * new object. - * - * This method is intentionally limited to same-origin responses, regardless of - * whether CORS was used or not. - * - * @param {Response} response - * @param {Function} modifier - * @memberof workbox-core - */ - async function copyResponse(response, modifier) { - let origin = null; - // If response.url isn't set, assume it's cross-origin and keep origin null. - if (response.url) { - const responseURL = new URL(response.url); - origin = responseURL.origin; - } - if (origin !== self.location.origin) { - throw new WorkboxError('cross-origin-copy-response', { - origin + // Wrap in try and catch in case the handle method throws a synchronous + // error. It should still callback to the catch handler. + let responsePromise; + try { + responsePromise = handler.handle({ + url, + request, + event, + params }); + } catch (err) { + responsePromise = Promise.reject(err); } - const clonedResponse = response.clone(); - // Create a fresh `ResponseInit` object by cloning the headers. - const responseInit = { - headers: new Headers(clonedResponse.headers), - status: clonedResponse.status, - statusText: clonedResponse.statusText - }; - // Apply any user modifications. - const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit; - // Create the new response from the body stream and `ResponseInit` - // modifications. Note: not all browsers support the Response.body stream, - // so fall back to reading the entire body into memory as a blob. - const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob(); - return new Response(body, modifiedResponseInit); - } - - /* - Copyright 2020 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - function stripParams(fullURL, ignoreParams) { - const strippedURL = new URL(fullURL); - for (const param of ignoreParams) { - strippedURL.searchParams.delete(param); - } - return strippedURL.href; - } - /** - * Matches an item in the cache, ignoring specific URL params. This is similar - * to the `ignoreSearch` option, but it allows you to ignore just specific - * params (while continuing to match on the others). - * - * @private - * @param {Cache} cache - * @param {Request} request - * @param {Object} matchOptions - * @param {Array} ignoreParams - * @return {Promise} - */ - async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) { - const strippedRequestURL = stripParams(request.url, ignoreParams); - // If the request doesn't include any ignored params, match as normal. - if (request.url === strippedRequestURL) { - return cache.match(request, matchOptions); - } - // Otherwise, match by comparing keys - const keysOptions = Object.assign(Object.assign({}, matchOptions), { - ignoreSearch: true - }); - const cacheKeys = await cache.keys(request, keysOptions); - for (const cacheKey of cacheKeys) { - const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams); - if (strippedRequestURL === strippedCacheKeyURL) { - return cache.match(cacheKey, matchOptions); - } - } - return; - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The Deferred class composes Promises in a way that allows for them to be - * resolved or rejected from outside the constructor. In most cases promises - * should be used directly, but Deferreds can be necessary when the logic to - * resolve a promise must be separate. - * - * @private - */ - class Deferred { - /** - * Creates a promise and exposes its resolve and reject functions as methods. - */ - constructor() { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - // Callbacks to be executed whenever there's a quota error. - // Can't change Function type right now. - // eslint-disable-next-line @typescript-eslint/ban-types - const quotaErrorCallbacks = new Set(); - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Runs all of the callback functions, one at a time sequentially, in the order - * in which they were registered. - * - * @memberof workbox-core - * @private - */ - async function executeQuotaErrorCallbacks() { - { - logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); - } - for (const callback of quotaErrorCallbacks) { - await callback(); - { - logger.log(callback, 'is complete.'); - } - } - { - logger.log('Finished running callbacks.'); - } - } - - /* - Copyright 2019 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Returns a promise that resolves and the passed number of milliseconds. - * This utility is an async/await-friendly version of `setTimeout`. - * - * @param {number} ms - * @return {Promise} - * @private - */ - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - // @ts-ignore - try { - self['workbox:strategies:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - function toRequest(input) { - return typeof input === 'string' ? new Request(input) : input; - } - /** - * A class created every time a Strategy instance instance calls - * {@link workbox-strategies.Strategy~handle} or - * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and - * cache actions around plugin callbacks and keeps track of when the strategy - * is "done" (i.e. all added `event.waitUntil()` promises have resolved). - * - * @memberof workbox-strategies - */ - class StrategyHandler { - /** - * Creates a new instance associated with the passed strategy and event - * that's handling the request. - * - * The constructor also initializes the state that will be passed to each of - * the plugins handling this request. - * - * @param {workbox-strategies.Strategy} strategy - * @param {Object} options - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] The return value from the - * {@link workbox-routing~matchCallback} (if applicable). - */ - constructor(strategy, options) { - this._cacheKeys = {}; - /** - * The request the strategy is performing (passed to the strategy's - * `handle()` or `handleAll()` method). - * @name request - * @instance - * @type {Request} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * The event associated with this request. - * @name event - * @instance - * @type {ExtendableEvent} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * A `URL` instance of `request.url` (if passed to the strategy's - * `handle()` or `handleAll()` method). - * Note: the `url` param will be present if the strategy was invoked - * from a workbox `Route` object. - * @name url - * @instance - * @type {URL|undefined} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * A `param` value (if passed to the strategy's - * `handle()` or `handleAll()` method). - * Note: the `param` param will be present if the strategy was invoked - * from a workbox `Route` object and the - * {@link workbox-routing~matchCallback} returned - * a truthy value (it will be that value). - * @name params - * @instance - * @type {*|undefined} - * @memberof workbox-strategies.StrategyHandler - */ - { - finalAssertExports.isInstance(options.event, ExtendableEvent, { - moduleName: 'workbox-strategies', - className: 'StrategyHandler', - funcName: 'constructor', - paramName: 'options.event' - }); - } - Object.assign(this, options); - this.event = options.event; - this._strategy = strategy; - this._handlerDeferred = new Deferred(); - this._extendLifetimePromises = []; - // Copy the plugins list (since it's mutable on the strategy), - // so any mutations don't affect this handler instance. - this._plugins = [...strategy.plugins]; - this._pluginStateMap = new Map(); - for (const plugin of this._plugins) { - this._pluginStateMap.set(plugin, {}); - } - this.event.waitUntil(this._handlerDeferred.promise); - } - /** - * Fetches a given request (and invokes any applicable plugin callback - * methods) using the `fetchOptions` (for non-navigation requests) and - * `plugins` defined on the `Strategy` object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - `requestWillFetch()` - * - `fetchDidSucceed()` - * - `fetchDidFail()` - * - * @param {Request|string} input The URL or request to fetch. - * @return {Promise} - */ - async fetch(input) { - const { - event - } = this; - let request = toRequest(input); - if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) { - const possiblePreloadResponse = await event.preloadResponse; - if (possiblePreloadResponse) { + // Get route's catch handler, if it exists + const catchHandler = route && route.catchHandler; + if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { + responsePromise = responsePromise.catch(async (err) => { + // If there's a route catch handler, process that first + if (catchHandler) { { - logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed( + `Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.` + ); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + try { + return await catchHandler.handle({ + url, + request, + event, + params + }); + } catch (catchErr) { + if (catchErr instanceof Error) { + err = catchErr; + } } - return possiblePreloadResponse; } - } - // If there is a fetchDidFail plugin, we need to save a clone of the - // original request before it's either modified by a requestWillFetch - // plugin or before the original request's body is consumed via fetch(). - const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null; - try { - for (const cb of this.iterateCallbacks('requestWillFetch')) { - request = await cb({ - request: request.clone(), + if (this._catchHandler) { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed( + `Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.` + ); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + return this._catchHandler.handle({ + url, + request, event }); } - } catch (err) { - if (err instanceof Error) { - throw new WorkboxError('plugin-error-request-will-fetch', { - thrownErrorMessage: err.message - }); - } - } - // The request can be altered by plugins with `requestWillFetch` making - // the original request (most likely from a `fetch` event) different - // from the Request we make. Pass both to `fetchDidFail` to aid debugging. - const pluginFilteredRequest = request.clone(); - try { - let fetchResponse; - // See https://github.com/GoogleChrome/workbox/issues/1796 - fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions); - if ("development" !== 'production') { - logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); - } - for (const callback of this.iterateCallbacks('fetchDidSucceed')) { - fetchResponse = await callback({ - event, - request: pluginFilteredRequest, - response: fetchResponse - }); - } - return fetchResponse; - } catch (error) { - { - logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); - } - // `originalRequest` will only exist if a `fetchDidFail` callback - // is being used (see above). - if (originalRequest) { - await this.runCallbacks('fetchDidFail', { - error: error, - event, - originalRequest: originalRequest.clone(), - request: pluginFilteredRequest.clone() - }); - } - throw error; - } - } - /** - * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on - * the response generated by `this.fetch()`. - * - * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, - * so you do not have to manually call `waitUntil()` on the event. - * - * @param {Request|string} input The request or URL to fetch and cache. - * @return {Promise} - */ - async fetchAndCachePut(input) { - const response = await this.fetch(input); - const responseClone = response.clone(); - void this.waitUntil(this.cachePut(input, responseClone)); - return response; - } - /** - * Matches a request from the cache (and invokes any applicable plugin - * callback methods) using the `cacheName`, `matchOptions`, and `plugins` - * defined on the strategy object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - cacheKeyWillByUsed() - * - cachedResponseWillByUsed() - * - * @param {Request|string} key The Request or URL to use as the cache key. - * @return {Promise} A matching response, if found. - */ - async cacheMatch(key) { - const request = toRequest(key); - let cachedResponse; - const { - cacheName, - matchOptions - } = this._strategy; - const effectiveRequest = await this.getCacheKey(request, 'read'); - const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { - cacheName + throw err; }); - cachedResponse = await caches.match(effectiveRequest, multiMatchOptions); + } + return responsePromise; + } + /** + * Checks a request and URL (and optionally an event) against the list of + * registered routes, and if there's a match, returns the corresponding + * route along with any params generated by the match. + * + * @param {Object} options + * @param {URL} options.url + * @param {boolean} options.sameOrigin The result of comparing `url.origin` + * against the current origin. + * @param {Request} options.request The request to match. + * @param {Event} options.event The corresponding event. + * @return {Object} An object with `route` and `params` properties. + * They are populated if a matching route was found or `undefined` + * otherwise. + */ + findMatchingRoute({ url, sameOrigin, request, event }) { + const routes = this._routes.get(request.method) || []; + for (const route of routes) { + let params; + // route.match returns type any, not possible to change right now. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const matchResult = route.match({ + url, + sameOrigin, + request, + event + }); + if (matchResult) { + { + // Warn developers that using an async matchCallback is almost always + // not the right thing to do. + if (matchResult instanceof Promise) { + logger.warn( + `While routing ${getFriendlyURL(url)}, an async ` + + `matchCallback function was used. Please convert the ` + + `following route to use a synchronous matchCallback function:`, + route + ); + } + } + // See https://github.com/GoogleChrome/workbox/issues/2079 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + params = matchResult; + if (Array.isArray(params) && params.length === 0) { + // Instead of passing an empty array in as params, use undefined. + params = undefined; + } else if ( + matchResult.constructor === Object && + // eslint-disable-line + Object.keys(matchResult).length === 0 + ) { + // Instead of passing an empty object in as params, use undefined. + params = undefined; + } else if (typeof matchResult === "boolean") { + // For the boolean value true (rather than just something truth-y), + // don't set params. + // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 + params = undefined; + } + // Return early if have a match. + return { + route, + params + }; + } + } + // If no match was found above, return and empty object. + return {}; + } + /** + * Define a default `handler` that's called when no routes explicitly + * match the incoming request. + * + * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. + * + * Without a default handler, unmatched requests will go against the + * network as if there were no service worker present. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to associate with this + * default handler. Each method has its own default. + */ + setDefaultHandler(handler, method = defaultMethod) { + this._defaultHandlerMap.set(method, normalizeHandler(handler)); + } + /** + * If a Route throws an error while handling a request, this `handler` + * will be called and given a chance to provide a response. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + */ + setCatchHandler(handler) { + this._catchHandler = normalizeHandler(handler); + } + /** + * Registers a route with the router. + * + * @param {workbox-routing.Route} route The route to register. + */ + registerRoute(route) { + { + finalAssertExports.isType(route, "object", { + moduleName: "workbox-routing", + className: "Router", + funcName: "registerRoute", + paramName: "route" + }); + finalAssertExports.hasMethod(route, "match", { + moduleName: "workbox-routing", + className: "Router", + funcName: "registerRoute", + paramName: "route" + }); + finalAssertExports.isType(route.handler, "object", { + moduleName: "workbox-routing", + className: "Router", + funcName: "registerRoute", + paramName: "route" + }); + finalAssertExports.hasMethod(route.handler, "handle", { + moduleName: "workbox-routing", + className: "Router", + funcName: "registerRoute", + paramName: "route.handler" + }); + finalAssertExports.isType(route.method, "string", { + moduleName: "workbox-routing", + className: "Router", + funcName: "registerRoute", + paramName: "route.method" + }); + } + if (!this._routes.has(route.method)) { + this._routes.set(route.method, []); + } + // Give precedence to all of the earlier routes by adding this additional + // route to the end of the array. + this._routes.get(route.method).push(route); + } + /** + * Unregisters a route with the router. + * + * @param {workbox-routing.Route} route The route to unregister. + */ + unregisterRoute(route) { + if (!this._routes.has(route.method)) { + throw new WorkboxError("unregister-route-but-not-found-with-method", { + method: route.method + }); + } + const routeIndex = this._routes.get(route.method).indexOf(route); + if (routeIndex > -1) { + this._routes.get(route.method).splice(routeIndex, 1); + } else { + throw new WorkboxError("unregister-route-route-not-registered"); + } + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let defaultRouter; + /** + * Creates a new, singleton Router instance if one does not exist. If one + * does already exist, that instance is returned. + * + * @private + * @return {Router} + */ + const getOrCreateDefaultRouter = () => { + if (!defaultRouter) { + defaultRouter = new Router(); + // The helpers that use the default Router assume these listeners exist. + defaultRouter.addFetchListener(); + defaultRouter.addCacheListener(); + } + return defaultRouter; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Easily register a RegExp, string, or function with a caching + * strategy to a singleton Router instance. + * + * This method will generate a Route for you if needed and + * call {@link workbox-routing.Router#registerRoute}. + * + * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture + * If the capture param is a `Route`, all other arguments will be ignored. + * @param {workbox-routing~handlerCallback} [handler] A callback + * function that returns a Promise resulting in a Response. This parameter + * is required if `capture` is not a `Route` object. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + * @return {workbox-routing.Route} The generated `Route`. + * + * @memberof workbox-routing + */ + function registerRoute(capture, handler, method) { + let route; + if (typeof capture === "string") { + const captureUrl = new URL(capture, location.href); + { + if (!(capture.startsWith("/") || capture.startsWith("http"))) { + throw new WorkboxError("invalid-string", { + moduleName: "workbox-routing", + funcName: "registerRoute", + paramName: "capture" + }); + } + // We want to check if Express-style wildcards are in the pathname only. + // TODO: Remove this log message in v4. + const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture; + // See https://github.com/pillarjs/path-to-regexp#parameters + const wildcards = "[*:?+]"; + if (new RegExp(`${wildcards}`).exec(valueToCheck)) { + logger.debug( + `The '$capture' parameter contains an Express-style wildcard ` + + `character (${wildcards}). Strings are now always interpreted as ` + + `exact matches; use a RegExp for partial or wildcard matches.` + ); + } + } + const matchCallback = ({ url }) => { { - if (cachedResponse) { - logger.debug(`Found a cached response in '${cacheName}'.`); - } else { - logger.debug(`No cached response found in '${cacheName}'.`); + if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { + logger.debug( + `${capture} only partially matches the cross-origin URL ` + + `${url.toString()}. This route will only handle cross-origin requests ` + + `if they match the entire URL.` + ); } } - for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) { - cachedResponse = (await callback({ + return url.href === captureUrl.href; + }; + // If `capture` is a string then `handler` and `method` must be present. + route = new Route(matchCallback, handler, method); + } else if (capture instanceof RegExp) { + // If `capture` is a `RegExp` then `handler` and `method` must be present. + route = new RegExpRoute(capture, handler, method); + } else if (typeof capture === "function") { + // If `capture` is a function then `handler` and `method` must be present. + route = new Route(capture, handler, method); + } else if (capture instanceof Route) { + route = capture; + } else { + throw new WorkboxError("unsupported-route-type", { + moduleName: "workbox-routing", + funcName: "registerRoute", + paramName: "capture" + }); + } + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.registerRoute(route); + return route; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const _cacheNameDetails = { + googleAnalytics: "googleAnalytics", + precache: "precache-v2", + prefix: "workbox", + runtime: "runtime", + suffix: typeof registration !== "undefined" ? registration.scope : "" + }; + const _createCacheName = (cacheName) => { + return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix] + .filter((value) => value && value.length > 0) + .join("-"); + }; + const eachCacheNameDetail = (fn) => { + for (const key of Object.keys(_cacheNameDetails)) { + fn(key); + } + }; + const cacheNames = { + updateDetails: (details) => { + eachCacheNameDetail((key) => { + if (typeof details[key] === "string") { + _cacheNameDetails[key] = details[key]; + } + }); + }, + getGoogleAnalyticsName: (userCacheName) => { + return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); + }, + getPrecacheName: (userCacheName) => { + return userCacheName || _createCacheName(_cacheNameDetails.precache); + }, + getPrefix: () => { + return _cacheNameDetails.prefix; + }, + getRuntimeName: (userCacheName) => { + return userCacheName || _createCacheName(_cacheNameDetails.runtime); + }, + getSuffix: () => { + return _cacheNameDetails.suffix; + } + }; + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A utility method that makes it easier to use `event.waitUntil` with + * async functions and return the result. + * + * @param {ExtendableEvent} event + * @param {Function} asyncFn + * @return {Function} + * @private + */ + function waitUntil(event, asyncFn) { + const returnPromise = asyncFn(); + event.waitUntil(returnPromise); + return returnPromise; + } + + // @ts-ignore + try { + self["workbox:precaching:7.0.0"] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Name of the search parameter used to store revision info. + const REVISION_SEARCH_PARAM = "__WB_REVISION__"; + /** + * Converts a manifest entry into a versioned URL suitable for precaching. + * + * @param {Object|string} entry + * @return {string} A URL with versioning info. + * + * @private + * @memberof workbox-precaching + */ + function createCacheKey(entry) { + if (!entry) { + throw new WorkboxError("add-to-cache-list-unexpected-type", { + entry + }); + } + // If a precache manifest entry is a string, it's assumed to be a versioned + // URL, like '/app.abcd1234.js'. Return as-is. + if (typeof entry === "string") { + const urlObject = new URL(entry, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + const { revision, url } = entry; + if (!url) { + throw new WorkboxError("add-to-cache-list-unexpected-type", { + entry + }); + } + // If there's just a URL and no revision, then it's also assumed to be a + // versioned URL. + if (!revision) { + const urlObject = new URL(url, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + // Otherwise, construct a properly versioned URL using the custom Workbox + // search parameter along with the revision info. + const cacheKeyURL = new URL(url, location.href); + const originalURL = new URL(url, location.href); + cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); + return { + cacheKey: cacheKeyURL.href, + url: originalURL.href + }; + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to determine the + * of assets that were updated (or not updated) during the install event. + * + * @private + */ + class PrecacheInstallReportPlugin { + constructor() { + this.updatedURLs = []; + this.notUpdatedURLs = []; + this.handlerWillStart = async ({ request, state }) => { + // TODO: `state` should never be undefined... + if (state) { + state.originalRequest = request; + } + }; + this.cachedResponseWillBeUsed = async ({ event, state, cachedResponse }) => { + if (event.type === "install") { + if (state && state.originalRequest && state.originalRequest instanceof Request) { + // TODO: `state` should never be undefined... + const url = state.originalRequest.url; + if (cachedResponse) { + this.notUpdatedURLs.push(url); + } else { + this.updatedURLs.push(url); + } + } + } + return cachedResponse; + }; + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to translate URLs into + * the corresponding cache key, based on the current revision info. + * + * @private + */ + class PrecacheCacheKeyPlugin { + constructor({ precacheController }) { + this.cacheKeyWillBeUsed = async ({ request, params }) => { + // Params is type any, can't change right now. + /* eslint-disable */ + const cacheKey = + (params === null || params === void 0 ? void 0 : params.cacheKey) || + this._precacheController.getCacheKeyForURL(request.url); + /* eslint-enable */ + return cacheKey + ? new Request(cacheKey, { + headers: request.headers + }) + : request; + }; + this._precacheController = precacheController; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} deletedURLs + * + * @private + */ + const logGroup = (groupTitle, deletedURLs) => { + logger.groupCollapsed(groupTitle); + for (const url of deletedURLs) { + logger.log(url); + } + logger.groupEnd(); + }; + /** + * @param {Array} deletedURLs + * + * @private + * @memberof workbox-precaching + */ + function printCleanupDetails(deletedURLs) { + const deletionCount = deletedURLs.length; + if (deletionCount > 0) { + logger.groupCollapsed( + `During precaching cleanup, ` + + `${deletionCount} cached ` + + `request${deletionCount === 1 ? " was" : "s were"} deleted.` + ); + logGroup("Deleted Cache Requests", deletedURLs); + logger.groupEnd(); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} urls + * + * @private + */ + function _nestedGroup(groupTitle, urls) { + if (urls.length === 0) { + return; + } + logger.groupCollapsed(groupTitle); + for (const url of urls) { + logger.log(url); + } + logger.groupEnd(); + } + /** + * @param {Array} urlsToPrecache + * @param {Array} urlsAlreadyPrecached + * + * @private + * @memberof workbox-precaching + */ + function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { + const precachedCount = urlsToPrecache.length; + const alreadyPrecachedCount = urlsAlreadyPrecached.length; + if (precachedCount || alreadyPrecachedCount) { + let message = `Precaching ${precachedCount} file${precachedCount === 1 ? "" : "s"}.`; + if (alreadyPrecachedCount > 0) { + message += + ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? " is" : "s are"} already cached.`; + } + logger.groupCollapsed(message); + _nestedGroup(`View newly precached URLs.`, urlsToPrecache); + _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); + logger.groupEnd(); + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let supportStatus; + /** + * A utility function that determines whether the current browser supports + * constructing a new `Response` from a `response.body` stream. + * + * @return {boolean} `true`, if the current browser can successfully + * construct a `Response` from a `response.body` stream, `false` otherwise. + * + * @private + */ + function canConstructResponseFromBodyStream() { + if (supportStatus === undefined) { + const testResponse = new Response(""); + if ("body" in testResponse) { + try { + new Response(testResponse.body); + supportStatus = true; + } catch (error) { + supportStatus = false; + } + } + supportStatus = false; + } + return supportStatus; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Allows developers to copy a response and modify its `headers`, `status`, + * or `statusText` values (the values settable via a + * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax} + * object in the constructor). + * To modify these values, pass a function as the second argument. That + * function will be invoked with a single object with the response properties + * `{headers, status, statusText}`. The return value of this function will + * be used as the `ResponseInit` for the new `Response`. To change the values + * either modify the passed parameter(s) and return it, or return a totally + * new object. + * + * This method is intentionally limited to same-origin responses, regardless of + * whether CORS was used or not. + * + * @param {Response} response + * @param {Function} modifier + * @memberof workbox-core + */ + async function copyResponse(response, modifier) { + let origin = null; + // If response.url isn't set, assume it's cross-origin and keep origin null. + if (response.url) { + const responseURL = new URL(response.url); + origin = responseURL.origin; + } + if (origin !== self.location.origin) { + throw new WorkboxError("cross-origin-copy-response", { + origin + }); + } + const clonedResponse = response.clone(); + // Create a fresh `ResponseInit` object by cloning the headers. + const responseInit = { + headers: new Headers(clonedResponse.headers), + status: clonedResponse.status, + statusText: clonedResponse.statusText + }; + // Apply any user modifications. + const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit; + // Create the new response from the body stream and `ResponseInit` + // modifications. Note: not all browsers support the Response.body stream, + // so fall back to reading the entire body into memory as a blob. + const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob(); + return new Response(body, modifiedResponseInit); + } + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function stripParams(fullURL, ignoreParams) { + const strippedURL = new URL(fullURL); + for (const param of ignoreParams) { + strippedURL.searchParams.delete(param); + } + return strippedURL.href; + } + /** + * Matches an item in the cache, ignoring specific URL params. This is similar + * to the `ignoreSearch` option, but it allows you to ignore just specific + * params (while continuing to match on the others). + * + * @private + * @param {Cache} cache + * @param {Request} request + * @param {Object} matchOptions + * @param {Array} ignoreParams + * @return {Promise} + */ + async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) { + const strippedRequestURL = stripParams(request.url, ignoreParams); + // If the request doesn't include any ignored params, match as normal. + if (request.url === strippedRequestURL) { + return cache.match(request, matchOptions); + } + // Otherwise, match by comparing keys + const keysOptions = Object.assign(Object.assign({}, matchOptions), { + ignoreSearch: true + }); + const cacheKeys = await cache.keys(request, keysOptions); + for (const cacheKey of cacheKeys) { + const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams); + if (strippedRequestURL === strippedCacheKeyURL) { + return cache.match(cacheKey, matchOptions); + } + } + return; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + class Deferred { + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Callbacks to be executed whenever there's a quota error. + // Can't change Function type right now. + // eslint-disable-next-line @typescript-eslint/ban-types + const quotaErrorCallbacks = new Set(); + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Runs all of the callback functions, one at a time sequentially, in the order + * in which they were registered. + * + * @memberof workbox-core + * @private + */ + async function executeQuotaErrorCallbacks() { + { + logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); + } + for (const callback of quotaErrorCallbacks) { + await callback(); + { + logger.log(callback, "is complete."); + } + } + { + logger.log("Finished running callbacks."); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Returns a promise that resolves and the passed number of milliseconds. + * This utility is an async/await-friendly version of `setTimeout`. + * + * @param {number} ms + * @return {Promise} + * @private + */ + function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + // @ts-ignore + try { + self["workbox:strategies:7.0.0"] && _(); + } catch (e) {} + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function toRequest(input) { + return typeof input === "string" ? new Request(input) : input; + } + /** + * A class created every time a Strategy instance instance calls + * {@link workbox-strategies.Strategy~handle} or + * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and + * cache actions around plugin callbacks and keeps track of when the strategy + * is "done" (i.e. all added `event.waitUntil()` promises have resolved). + * + * @memberof workbox-strategies + */ + class StrategyHandler { + /** + * Creates a new instance associated with the passed strategy and event + * that's handling the request. + * + * The constructor also initializes the state that will be passed to each of + * the plugins handling this request. + * + * @param {workbox-strategies.Strategy} strategy + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] The return value from the + * {@link workbox-routing~matchCallback} (if applicable). + */ + constructor(strategy, options) { + this._cacheKeys = {}; + /** + * The request the strategy is performing (passed to the strategy's + * `handle()` or `handleAll()` method). + * @name request + * @instance + * @type {Request} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * The event associated with this request. + * @name event + * @instance + * @type {ExtendableEvent} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `URL` instance of `request.url` (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `url` param will be present if the strategy was invoked + * from a workbox `Route` object. + * @name url + * @instance + * @type {URL|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `param` value (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `param` param will be present if the strategy was invoked + * from a workbox `Route` object and the + * {@link workbox-routing~matchCallback} returned + * a truthy value (it will be that value). + * @name params + * @instance + * @type {*|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + { + finalAssertExports.isInstance(options.event, ExtendableEvent, { + moduleName: "workbox-strategies", + className: "StrategyHandler", + funcName: "constructor", + paramName: "options.event" + }); + } + Object.assign(this, options); + this.event = options.event; + this._strategy = strategy; + this._handlerDeferred = new Deferred(); + this._extendLifetimePromises = []; + // Copy the plugins list (since it's mutable on the strategy), + // so any mutations don't affect this handler instance. + this._plugins = [...strategy.plugins]; + this._pluginStateMap = new Map(); + for (const plugin of this._plugins) { + this._pluginStateMap.set(plugin, {}); + } + this.event.waitUntil(this._handlerDeferred.promise); + } + /** + * Fetches a given request (and invokes any applicable plugin callback + * methods) using the `fetchOptions` (for non-navigation requests) and + * `plugins` defined on the `Strategy` object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - `requestWillFetch()` + * - `fetchDidSucceed()` + * - `fetchDidFail()` + * + * @param {Request|string} input The URL or request to fetch. + * @return {Promise} + */ + async fetch(input) { + const { event } = this; + let request = toRequest(input); + if (request.mode === "navigate" && event instanceof FetchEvent && event.preloadResponse) { + const possiblePreloadResponse = await event.preloadResponse; + if (possiblePreloadResponse) { + { + logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); + } + return possiblePreloadResponse; + } + } + // If there is a fetchDidFail plugin, we need to save a clone of the + // original request before it's either modified by a requestWillFetch + // plugin or before the original request's body is consumed via fetch(). + const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null; + try { + for (const cb of this.iterateCallbacks("requestWillFetch")) { + request = await cb({ + request: request.clone(), + event + }); + } + } catch (err) { + if (err instanceof Error) { + throw new WorkboxError("plugin-error-request-will-fetch", { + thrownErrorMessage: err.message + }); + } + } + // The request can be altered by plugins with `requestWillFetch` making + // the original request (most likely from a `fetch` event) different + // from the Request we make. Pass both to `fetchDidFail` to aid debugging. + const pluginFilteredRequest = request.clone(); + try { + let fetchResponse; + // See https://github.com/GoogleChrome/workbox/issues/1796 + fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions); + if ("development" !== "production") { + logger.debug( + `Network request for ` + + `'${getFriendlyURL(request.url)}' returned a response with ` + + `status '${fetchResponse.status}'.` + ); + } + for (const callback of this.iterateCallbacks("fetchDidSucceed")) { + fetchResponse = await callback({ + event, + request: pluginFilteredRequest, + response: fetchResponse + }); + } + return fetchResponse; + } catch (error) { + { + logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); + } + // `originalRequest` will only exist if a `fetchDidFail` callback + // is being used (see above). + if (originalRequest) { + await this.runCallbacks("fetchDidFail", { + error: error, + event, + originalRequest: originalRequest.clone(), + request: pluginFilteredRequest.clone() + }); + } + throw error; + } + } + /** + * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on + * the response generated by `this.fetch()`. + * + * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, + * so you do not have to manually call `waitUntil()` on the event. + * + * @param {Request|string} input The request or URL to fetch and cache. + * @return {Promise} + */ + async fetchAndCachePut(input) { + const response = await this.fetch(input); + const responseClone = response.clone(); + void this.waitUntil(this.cachePut(input, responseClone)); + return response; + } + /** + * Matches a request from the cache (and invokes any applicable plugin + * callback methods) using the `cacheName`, `matchOptions`, and `plugins` + * defined on the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillByUsed() + * - cachedResponseWillByUsed() + * + * @param {Request|string} key The Request or URL to use as the cache key. + * @return {Promise} A matching response, if found. + */ + async cacheMatch(key) { + const request = toRequest(key); + let cachedResponse; + const { cacheName, matchOptions } = this._strategy; + const effectiveRequest = await this.getCacheKey(request, "read"); + const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { + cacheName + }); + cachedResponse = await caches.match(effectiveRequest, multiMatchOptions); + { + if (cachedResponse) { + logger.debug(`Found a cached response in '${cacheName}'.`); + } else { + logger.debug(`No cached response found in '${cacheName}'.`); + } + } + for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")) { + cachedResponse = + (await callback({ cacheName, matchOptions, cachedResponse, request: effectiveRequest, event: this.event })) || undefined; - } - return cachedResponse; } - /** - * Puts a request/response pair in the cache (and invokes any applicable - * plugin callback methods) using the `cacheName` and `plugins` defined on - * the strategy object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - cacheKeyWillByUsed() - * - cacheWillUpdate() - * - cacheDidUpdate() - * - * @param {Request|string} key The request or URL to use as the cache key. - * @param {Response} response The response to cache. - * @return {Promise} `false` if a cacheWillUpdate caused the response - * not be cached, and `true` otherwise. - */ - async cachePut(key, response) { - const request = toRequest(key); - // Run in the next task to avoid blocking other cache reads. - // https://github.com/w3c/ServiceWorker/issues/1397 - await timeout(0); - const effectiveRequest = await this.getCacheKey(request, 'write'); - { - if (effectiveRequest.method && effectiveRequest.method !== 'GET') { - throw new WorkboxError('attempt-to-cache-non-get-request', { - url: getFriendlyURL(effectiveRequest.url), - method: effectiveRequest.method - }); - } - // See https://github.com/GoogleChrome/workbox/issues/2818 - const vary = response.headers.get('Vary'); - if (vary) { - logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`); - } - } - if (!response) { - { - logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); - } - throw new WorkboxError('cache-put-with-no-response', { - url: getFriendlyURL(effectiveRequest.url) + return cachedResponse; + } + /** + * Puts a request/response pair in the cache (and invokes any applicable + * plugin callback methods) using the `cacheName` and `plugins` defined on + * the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillByUsed() + * - cacheWillUpdate() + * - cacheDidUpdate() + * + * @param {Request|string} key The request or URL to use as the cache key. + * @param {Response} response The response to cache. + * @return {Promise} `false` if a cacheWillUpdate caused the response + * not be cached, and `true` otherwise. + */ + async cachePut(key, response) { + const request = toRequest(key); + // Run in the next task to avoid blocking other cache reads. + // https://github.com/w3c/ServiceWorker/issues/1397 + await timeout(0); + const effectiveRequest = await this.getCacheKey(request, "write"); + { + if (effectiveRequest.method && effectiveRequest.method !== "GET") { + throw new WorkboxError("attempt-to-cache-non-get-request", { + url: getFriendlyURL(effectiveRequest.url), + method: effectiveRequest.method }); } - const responseToCache = await this._ensureResponseSafeToCache(response); - if (!responseToCache) { - { - logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache); - } - return false; + // See https://github.com/GoogleChrome/workbox/issues/2818 + const vary = response.headers.get("Vary"); + if (vary) { + logger.debug( + `The response for ${getFriendlyURL(effectiveRequest.url)} ` + + `has a 'Vary: ${vary}' header. ` + + `Consider setting the {ignoreVary: true} option on your strategy ` + + `to ensure cache matching and deletion works as expected.` + ); } - const { + } + if (!response) { + { + logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); + } + throw new WorkboxError("cache-put-with-no-response", { + url: getFriendlyURL(effectiveRequest.url) + }); + } + const responseToCache = await this._ensureResponseSafeToCache(response); + if (!responseToCache) { + { + logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache); + } + return false; + } + const { cacheName, matchOptions } = this._strategy; + const cache = await self.caches.open(cacheName); + const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate"); + const oldResponse = hasCacheUpdateCallback + ? await cacheMatchIgnoreParams( + // TODO(philipwalton): the `__WB_REVISION__` param is a precaching + // feature. Consider into ways to only add this behavior if using + // precaching. + cache, + effectiveRequest.clone(), + ["__WB_REVISION__"], + matchOptions + ) + : null; + { + logger.debug( + `Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.` + ); + } + try { + await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache); + } catch (error) { + if (error instanceof Error) { + // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError + if (error.name === "QuotaExceededError") { + await executeQuotaErrorCallbacks(); + } + throw error; + } + } + for (const callback of this.iterateCallbacks("cacheDidUpdate")) { + await callback({ cacheName, - matchOptions - } = this._strategy; - const cache = await self.caches.open(cacheName); - const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate'); - const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams( - // TODO(philipwalton): the `__WB_REVISION__` param is a precaching - // feature. Consider into ways to only add this behavior if using - // precaching. - cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null; - { - logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`); - } - try { - await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache); - } catch (error) { - if (error instanceof Error) { - // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError - if (error.name === 'QuotaExceededError') { - await executeQuotaErrorCallbacks(); - } - throw error; - } - } - for (const callback of this.iterateCallbacks('cacheDidUpdate')) { - await callback({ - cacheName, - oldResponse, - newResponse: responseToCache.clone(), - request: effectiveRequest, - event: this.event - }); - } - return true; + oldResponse, + newResponse: responseToCache.clone(), + request: effectiveRequest, + event: this.event + }); } - /** - * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and - * executes any of those callbacks found in sequence. The final `Request` - * object returned by the last plugin is treated as the cache key for cache - * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have - * been registered, the passed request is returned unmodified - * - * @param {Request} request - * @param {string} mode - * @return {Promise} - */ - async getCacheKey(request, mode) { - const key = `${request.url} | ${mode}`; - if (!this._cacheKeys[key]) { - let effectiveRequest = request; - for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) { - effectiveRequest = toRequest(await callback({ + return true; + } + /** + * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and + * executes any of those callbacks found in sequence. The final `Request` + * object returned by the last plugin is treated as the cache key for cache + * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have + * been registered, the passed request is returned unmodified + * + * @param {Request} request + * @param {string} mode + * @return {Promise} + */ + async getCacheKey(request, mode) { + const key = `${request.url} | ${mode}`; + if (!this._cacheKeys[key]) { + let effectiveRequest = request; + for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")) { + effectiveRequest = toRequest( + await callback({ mode, request: effectiveRequest, event: this.event, // params has a type any can't change right now. params: this.params // eslint-disable-line - })); - } - this._cacheKeys[key] = effectiveRequest; + }) + ); } - return this._cacheKeys[key]; + this._cacheKeys[key] = effectiveRequest; } - /** - * Returns true if the strategy has at least one plugin with the given - * callback. - * - * @param {string} name The name of the callback to check for. - * @return {boolean} - */ - hasCallback(name) { - for (const plugin of this._strategy.plugins) { - if (name in plugin) { - return true; - } - } - return false; - } - /** - * Runs all plugin callbacks matching the given name, in order, passing the - * given param object (merged ith the current plugin state) as the only - * argument. - * - * Note: since this method runs all plugins, it's not suitable for cases - * where the return value of a callback needs to be applied prior to calling - * the next callback. See - * {@link workbox-strategies.StrategyHandler#iterateCallbacks} - * below for how to handle that case. - * - * @param {string} name The name of the callback to run within each plugin. - * @param {Object} param The object to pass as the first (and only) param - * when executing each callback. This object will be merged with the - * current plugin state prior to callback execution. - */ - async runCallbacks(name, param) { - for (const callback of this.iterateCallbacks(name)) { - // TODO(philipwalton): not sure why `any` is needed. It seems like - // this should work with `as WorkboxPluginCallbackParam[C]`. - await callback(param); + return this._cacheKeys[key]; + } + /** + * Returns true if the strategy has at least one plugin with the given + * callback. + * + * @param {string} name The name of the callback to check for. + * @return {boolean} + */ + hasCallback(name) { + for (const plugin of this._strategy.plugins) { + if (name in plugin) { + return true; } } - /** - * Accepts a callback and returns an iterable of matching plugin callbacks, - * where each callback is wrapped with the current handler state (i.e. when - * you call each callback, whatever object parameter you pass it will - * be merged with the plugin's current state). - * - * @param {string} name The name fo the callback to run - * @return {Array} - */ - *iterateCallbacks(name) { - for (const plugin of this._strategy.plugins) { - if (typeof plugin[name] === 'function') { - const state = this._pluginStateMap.get(plugin); - const statefulCallback = param => { - const statefulParam = Object.assign(Object.assign({}, param), { - state - }); - // TODO(philipwalton): not sure why `any` is needed. It seems like - // this should work with `as WorkboxPluginCallbackParam[C]`. - return plugin[name](statefulParam); - }; - yield statefulCallback; - } + return false; + } + /** + * Runs all plugin callbacks matching the given name, in order, passing the + * given param object (merged ith the current plugin state) as the only + * argument. + * + * Note: since this method runs all plugins, it's not suitable for cases + * where the return value of a callback needs to be applied prior to calling + * the next callback. See + * {@link workbox-strategies.StrategyHandler#iterateCallbacks} + * below for how to handle that case. + * + * @param {string} name The name of the callback to run within each plugin. + * @param {Object} param The object to pass as the first (and only) param + * when executing each callback. This object will be merged with the + * current plugin state prior to callback execution. + */ + async runCallbacks(name, param) { + for (const callback of this.iterateCallbacks(name)) { + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + await callback(param); + } + } + /** + * Accepts a callback and returns an iterable of matching plugin callbacks, + * where each callback is wrapped with the current handler state (i.e. when + * you call each callback, whatever object parameter you pass it will + * be merged with the plugin's current state). + * + * @param {string} name The name fo the callback to run + * @return {Array} + */ + *iterateCallbacks(name) { + for (const plugin of this._strategy.plugins) { + if (typeof plugin[name] === "function") { + const state = this._pluginStateMap.get(plugin); + const statefulCallback = (param) => { + const statefulParam = Object.assign(Object.assign({}, param), { + state + }); + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + return plugin[name](statefulParam); + }; + yield statefulCallback; } } - /** - * Adds a promise to the - * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises} - * of the event event associated with the request being handled (usually a - * `FetchEvent`). - * - * Note: you can await - * {@link workbox-strategies.StrategyHandler~doneWaiting} - * to know when all added promises have settled. - * - * @param {Promise} promise A promise to add to the extend lifetime promises - * of the event that triggered the request. - */ - waitUntil(promise) { - this._extendLifetimePromises.push(promise); - return promise; + } + /** + * Adds a promise to the + * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises} + * of the event event associated with the request being handled (usually a + * `FetchEvent`). + * + * Note: you can await + * {@link workbox-strategies.StrategyHandler~doneWaiting} + * to know when all added promises have settled. + * + * @param {Promise} promise A promise to add to the extend lifetime promises + * of the event that triggered the request. + */ + waitUntil(promise) { + this._extendLifetimePromises.push(promise); + return promise; + } + /** + * Returns a promise that resolves once all promises passed to + * {@link workbox-strategies.StrategyHandler~waitUntil} + * have settled. + * + * Note: any work done after `doneWaiting()` settles should be manually + * passed to an event's `waitUntil()` method (not this handler's + * `waitUntil()` method), otherwise the service worker thread my be killed + * prior to your work completing. + */ + async doneWaiting() { + let promise; + while ((promise = this._extendLifetimePromises.shift())) { + await promise; } - /** - * Returns a promise that resolves once all promises passed to - * {@link workbox-strategies.StrategyHandler~waitUntil} - * have settled. - * - * Note: any work done after `doneWaiting()` settles should be manually - * passed to an event's `waitUntil()` method (not this handler's - * `waitUntil()` method), otherwise the service worker thread my be killed - * prior to your work completing. - */ - async doneWaiting() { - let promise; - while (promise = this._extendLifetimePromises.shift()) { - await promise; - } - } - /** - * Stops running the strategy and immediately resolves any pending - * `waitUntil()` promises. - */ - destroy() { - this._handlerDeferred.resolve(null); - } - /** - * This method will call cacheWillUpdate on the available plugins (or use - * status === 200) to determine if the Response is safe and valid to cache. - * - * @param {Request} options.request - * @param {Response} options.response - * @return {Promise} - * - * @private - */ - async _ensureResponseSafeToCache(response) { - let responseToCache = response; - let pluginsUsed = false; - for (const callback of this.iterateCallbacks('cacheWillUpdate')) { - responseToCache = (await callback({ + } + /** + * Stops running the strategy and immediately resolves any pending + * `waitUntil()` promises. + */ + destroy() { + this._handlerDeferred.resolve(null); + } + /** + * This method will call cacheWillUpdate on the available plugins (or use + * status === 200) to determine if the Response is safe and valid to cache. + * + * @param {Request} options.request + * @param {Response} options.response + * @return {Promise} + * + * @private + */ + async _ensureResponseSafeToCache(response) { + let responseToCache = response; + let pluginsUsed = false; + for (const callback of this.iterateCallbacks("cacheWillUpdate")) { + responseToCache = + (await callback({ request: this.request, response: responseToCache, event: this.event })) || undefined; - pluginsUsed = true; - if (!responseToCache) { - break; - } + pluginsUsed = true; + if (!responseToCache) { + break; } - if (!pluginsUsed) { - if (responseToCache && responseToCache.status !== 200) { - responseToCache = undefined; - } - { - if (responseToCache) { - if (responseToCache.status !== 200) { - if (responseToCache.status === 0) { - logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`); - } else { - logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`); - } + } + if (!pluginsUsed) { + if (responseToCache && responseToCache.status !== 200) { + responseToCache = undefined; + } + { + if (responseToCache) { + if (responseToCache.status !== 200) { + if (responseToCache.status === 0) { + logger.warn( + `The response for '${this.request.url}' ` + + `is an opaque response. The caching strategy that you're ` + + `using will not cache opaque responses by default.` + ); + } else { + logger.debug( + `The response for '${this.request.url}' ` + + `returned a status code of '${response.status}' and won't ` + + `be cached as a result.` + ); } } } } - return responseToCache; } + return responseToCache; } + } - /* + /* Copyright 2020 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ + /** + * An abstract base class that all other strategy classes must extend from: + * + * @memberof workbox-strategies + */ + class Strategy { /** - * An abstract base class that all other strategy classes must extend from: + * Creates a new instance of the strategy and sets all documented option + * properties as public instance properties. * - * @memberof workbox-strategies + * Note: if a custom strategy class extends the base Strategy class and does + * not need more than these properties, it does not need to define its own + * constructor. + * + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) + * `fetch()` requests made by this strategy. + * @param {Object} [options.matchOptions] The + * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. */ - class Strategy { + constructor(options = {}) { /** - * Creates a new instance of the strategy and sets all documented option - * properties as public instance properties. - * - * Note: if a custom strategy class extends the base Strategy class and does - * not need more than these properties, it does not need to define its own - * constructor. - * - * @param {Object} [options] - * @param {string} [options.cacheName] Cache name to store and retrieve + * Cache name to store and retrieve * requests. Defaults to the cache names provided by * {@link workbox-core.cacheNames}. - * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * to use in conjunction with this caching strategy. - * @param {Object} [options.fetchOptions] Values passed along to the - * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) - * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) - * `fetch()` requests made by this strategy. - * @param {Object} [options.matchOptions] The + * + * @type {string} + */ + this.cacheName = cacheNames.getRuntimeName(options.cacheName); + /** + * The list + * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * used by this strategy. + * + * @type {Array} + */ + this.plugins = options.plugins || []; + /** + * Values passed along to the + * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters} + * of all fetch() requests made by this strategy. + * + * @type {Object} + */ + this.fetchOptions = options.fetchOptions; + /** + * The * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} * for any `cache.match()` or `cache.put()` calls made by this strategy. + * + * @type {Object} */ - constructor(options = {}) { - /** - * Cache name to store and retrieve - * requests. Defaults to the cache names provided by - * {@link workbox-core.cacheNames}. - * - * @type {string} - */ - this.cacheName = cacheNames.getRuntimeName(options.cacheName); - /** - * The list - * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * used by this strategy. - * - * @type {Array} - */ - this.plugins = options.plugins || []; - /** - * Values passed along to the - * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters} - * of all fetch() requests made by this strategy. - * - * @type {Object} - */ - this.fetchOptions = options.fetchOptions; - /** - * The - * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} - * for any `cache.match()` or `cache.put()` calls made by this strategy. - * - * @type {Object} - */ - this.matchOptions = options.matchOptions; - } - /** - * Perform a request strategy and returns a `Promise` that will resolve with - * a `Response`, invoking all relevant plugin callbacks. - * - * When a strategy instance is registered with a Workbox - * {@link workbox-routing.Route}, this method is automatically - * called when the route matches. - * - * Alternatively, this method can be used in a standalone `FetchEvent` - * listener by passing it to `event.respondWith()`. - * - * @param {FetchEvent|Object} options A `FetchEvent` or an object with the - * properties listed below. - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] - */ - handle(options) { - const [responseDone] = this.handleAll(options); - return responseDone; - } - /** - * Similar to {@link workbox-strategies.Strategy~handle}, but - * instead of just returning a `Promise` that resolves to a `Response` it - * it will return an tuple of `[response, done]` promises, where the former - * (`response`) is equivalent to what `handle()` returns, and the latter is a - * Promise that will resolve once any promises that were added to - * `event.waitUntil()` as part of performing the strategy have completed. - * - * You can await the `done` promise to ensure any extra work performed by - * the strategy (usually caching responses) completes successfully. - * - * @param {FetchEvent|Object} options A `FetchEvent` or an object with the - * properties listed below. - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] - * @return {Array} A tuple of [response, done] - * promises that can be used to determine when the response resolves as - * well as when the handler has completed all its work. - */ - handleAll(options) { - // Allow for flexible options to be passed. - if (options instanceof FetchEvent) { - options = { - event: options, - request: options.request - }; - } - const event = options.event; - const request = typeof options.request === 'string' ? new Request(options.request) : options.request; - const params = 'params' in options ? options.params : undefined; - const handler = new StrategyHandler(this, { - event, - request, - params - }); - const responseDone = this._getResponse(handler, request, event); - const handlerDone = this._awaitComplete(responseDone, handler, request, event); - // Return an array of promises, suitable for use with Promise.all(). - return [responseDone, handlerDone]; - } - async _getResponse(handler, request, event) { - await handler.runCallbacks('handlerWillStart', { - event, - request - }); - let response = undefined; - try { - response = await this._handle(request, handler); - // The "official" Strategy subclasses all throw this error automatically, - // but in case a third-party Strategy doesn't, ensure that we have a - // consistent failure when there's no response or an error response. - if (!response || response.type === 'error') { - throw new WorkboxError('no-response', { - url: request.url - }); - } - } catch (error) { - if (error instanceof Error) { - for (const callback of handler.iterateCallbacks('handlerDidError')) { - response = await callback({ - error, - event, - request - }); - if (response) { - break; - } - } - } - if (!response) { - throw error; - } else { - logger.log(`While responding to '${getFriendlyURL(request.url)}', ` + `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` + `a handlerDidError plugin.`); - } - } - for (const callback of handler.iterateCallbacks('handlerWillRespond')) { - response = await callback({ - event, - request, - response - }); - } - return response; - } - async _awaitComplete(responseDone, handler, request, event) { - let response; - let error; - try { - response = await responseDone; - } catch (error) { - // Ignore errors, as response errors should be caught via the `response` - // promise above. The `done` promise will only throw for errors in - // promises passed to `handler.waitUntil()`. - } - try { - await handler.runCallbacks('handlerDidRespond', { - event, - request, - response - }); - await handler.doneWaiting(); - } catch (waitUntilError) { - if (waitUntilError instanceof Error) { - error = waitUntilError; - } - } - await handler.runCallbacks('handlerDidComplete', { - event, - request, - response, - error: error - }); - handler.destroy(); - if (error) { - throw error; - } - } + this.matchOptions = options.matchOptions; } /** - * Classes extending the `Strategy` based class should implement this method, - * and leverage the {@link workbox-strategies.StrategyHandler} - * arg to perform all fetching and cache logic, which will ensure all relevant - * cache, cache options, fetch options and plugins are used (per the current - * strategy instance). + * Perform a request strategy and returns a `Promise` that will resolve with + * a `Response`, invoking all relevant plugin callbacks. * - * @name _handle - * @instance - * @abstract - * @function - * @param {Request} request - * @param {workbox-strategies.StrategyHandler} handler - * @return {Promise} + * When a strategy instance is registered with a Workbox + * {@link workbox-routing.Route}, this method is automatically + * called when the route matches. * - * @memberof workbox-strategies.Strategy + * Alternatively, this method can be used in a standalone `FetchEvent` + * listener by passing it to `event.respondWith()`. + * + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] */ - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ + handle(options) { + const [responseDone] = this.handleAll(options); + return responseDone; + } /** - * A {@link workbox-strategies.Strategy} implementation - * specifically designed to work with - * {@link workbox-precaching.PrecacheController} - * to both cache and fetch precached assets. + * Similar to {@link workbox-strategies.Strategy~handle}, but + * instead of just returning a `Promise` that resolves to a `Response` it + * it will return an tuple of `[response, done]` promises, where the former + * (`response`) is equivalent to what `handle()` returns, and the latter is a + * Promise that will resolve once any promises that were added to + * `event.waitUntil()` as part of performing the strategy have completed. * - * Note: an instance of this class is created automatically when creating a - * `PrecacheController`; it's generally not necessary to create this yourself. + * You can await the `done` promise to ensure any extra work performed by + * the strategy (usually caching responses) completes successfully. * - * @extends workbox-strategies.Strategy - * @memberof workbox-precaching + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] + * @return {Array} A tuple of [response, done] + * promises that can be used to determine when the response resolves as + * well as when the handler has completed all its work. */ - class PrecacheStrategy extends Strategy { - /** - * - * @param {Object} [options] - * @param {string} [options.cacheName] Cache name to store and retrieve - * requests. Defaults to the cache names provided by - * {@link workbox-core.cacheNames}. - * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins} - * to use in conjunction with this caching strategy. - * @param {Object} [options.fetchOptions] Values passed along to the - * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} - * of all fetch() requests made by this strategy. - * @param {Object} [options.matchOptions] The - * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions} - * for any `cache.match()` or `cache.put()` calls made by this strategy. - * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to - * get the response from the network if there's a precache miss. - */ - constructor(options = {}) { - options.cacheName = cacheNames.getPrecacheName(options.cacheName); - super(options); - this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true; - // Redirected responses cannot be used to satisfy a navigation request, so - // any redirected response must be "copied" rather than cloned, so the new - // response doesn't contain the `redirected` flag. See: - // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1 - this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin); + handleAll(options) { + // Allow for flexible options to be passed. + if (options instanceof FetchEvent) { + options = { + event: options, + request: options.request + }; } - /** - * @private - * @param {Request|string} request A request to run this strategy for. - * @param {workbox-strategies.StrategyHandler} handler The event that - * triggered the request. - * @return {Promise} - */ - async _handle(request, handler) { - const response = await handler.cacheMatch(request); - if (response) { - return response; - } - // If this is an `install` event for an entry that isn't already cached, - // then populate the cache. - if (handler.event && handler.event.type === 'install') { - return await this._handleInstall(request, handler); - } - // Getting here means something went wrong. An entry that should have been - // precached wasn't found in the cache. - return await this._handleFetch(request, handler); - } - async _handleFetch(request, handler) { - let response; - const params = handler.params || {}; - // Fall back to the network if we're configured to do so. - if (this._fallbackToNetwork) { - { - logger.warn(`The precached response for ` + `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` + `found. Falling back to the network.`); - } - const integrityInManifest = params.integrity; - const integrityInRequest = request.integrity; - const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest; - // Do not add integrity if the original request is no-cors - // See https://github.com/GoogleChrome/workbox/issues/3096 - response = await handler.fetch(new Request(request, { - integrity: request.mode !== 'no-cors' ? integrityInRequest || integrityInManifest : undefined - })); - // It's only "safe" to repair the cache if we're using SRI to guarantee - // that the response matches the precache manifest's expectations, - // and there's either a) no integrity property in the incoming request - // or b) there is an integrity, and it matches the precache manifest. - // See https://github.com/GoogleChrome/workbox/issues/2858 - // Also if the original request users no-cors we don't use integrity. - // See https://github.com/GoogleChrome/workbox/issues/3096 - if (integrityInManifest && noIntegrityConflict && request.mode !== 'no-cors') { - this._useDefaultCacheabilityPluginIfNeeded(); - const wasCached = await handler.cachePut(request, response.clone()); - { - if (wasCached) { - logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`); - } - } - } - } else { - // This shouldn't normally happen, but there are edge cases: - // https://github.com/GoogleChrome/workbox/issues/1441 - throw new WorkboxError('missing-precache-entry', { - cacheName: this.cacheName, + const event = options.event; + const request = typeof options.request === "string" ? new Request(options.request) : options.request; + const params = "params" in options ? options.params : undefined; + const handler = new StrategyHandler(this, { + event, + request, + params + }); + const responseDone = this._getResponse(handler, request, event); + const handlerDone = this._awaitComplete(responseDone, handler, request, event); + // Return an array of promises, suitable for use with Promise.all(). + return [responseDone, handlerDone]; + } + async _getResponse(handler, request, event) { + await handler.runCallbacks("handlerWillStart", { + event, + request + }); + let response = undefined; + try { + response = await this._handle(request, handler); + // The "official" Strategy subclasses all throw this error automatically, + // but in case a third-party Strategy doesn't, ensure that we have a + // consistent failure when there's no response or an error response. + if (!response || response.type === "error") { + throw new WorkboxError("no-response", { url: request.url }); } - { - const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read')); - // Workbox is going to handle the route. - // print the routing details to the console. - logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url)); - logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`); - logger.groupCollapsed(`View request details here.`); - logger.log(request); - logger.groupEnd(); - logger.groupCollapsed(`View response details here.`); - logger.log(response); - logger.groupEnd(); - logger.groupEnd(); - } - return response; - } - async _handleInstall(request, handler) { - this._useDefaultCacheabilityPluginIfNeeded(); - const response = await handler.fetch(request); - // Make sure we defer cachePut() until after we know the response - // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737 - const wasCached = await handler.cachePut(request, response.clone()); - if (!wasCached) { - // Throwing here will lead to the `install` handler failing, which - // we want to do if *any* of the responses aren't safe to cache. - throw new WorkboxError('bad-precaching-response', { - url: request.url, - status: response.status - }); - } - return response; - } - /** - * This method is complex, as there a number of things to account for: - * - * The `plugins` array can be set at construction, and/or it might be added to - * to at any time before the strategy is used. - * - * At the time the strategy is used (i.e. during an `install` event), there - * needs to be at least one plugin that implements `cacheWillUpdate` in the - * array, other than `copyRedirectedCacheableResponsesPlugin`. - * - * - If this method is called and there are no suitable `cacheWillUpdate` - * plugins, we need to add `defaultPrecacheCacheabilityPlugin`. - * - * - If this method is called and there is exactly one `cacheWillUpdate`, then - * we don't have to do anything (this might be a previously added - * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin). - * - * - If this method is called and there is more than one `cacheWillUpdate`, - * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so, - * we need to remove it. (This situation is unlikely, but it could happen if - * the strategy is used multiple times, the first without a `cacheWillUpdate`, - * and then later on after manually adding a custom `cacheWillUpdate`.) - * - * See https://github.com/GoogleChrome/workbox/issues/2737 for more context. - * - * @private - */ - _useDefaultCacheabilityPluginIfNeeded() { - let defaultPluginIndex = null; - let cacheWillUpdatePluginCount = 0; - for (const [index, plugin] of this.plugins.entries()) { - // Ignore the copy redirected plugin when determining what to do. - if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) { - continue; - } - // Save the default plugin's index, in case it needs to be removed. - if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) { - defaultPluginIndex = index; - } - if (plugin.cacheWillUpdate) { - cacheWillUpdatePluginCount++; - } - } - if (cacheWillUpdatePluginCount === 0) { - this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin); - } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) { - // Only remove the default plugin; multiple custom plugins are allowed. - this.plugins.splice(defaultPluginIndex, 1); - } - // Nothing needs to be done if cacheWillUpdatePluginCount is 1 - } - } - PrecacheStrategy.defaultPrecacheCacheabilityPlugin = { - async cacheWillUpdate({ - response - }) { - if (!response || response.status >= 400) { - return null; - } - return response; - } - }; - PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = { - async cacheWillUpdate({ - response - }) { - return response.redirected ? await copyResponse(response) : response; - } - }; - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Performs efficient precaching of assets. - * - * @memberof workbox-precaching - */ - class PrecacheController { - /** - * Create a new PrecacheController. - * - * @param {Object} [options] - * @param {string} [options.cacheName] The cache to use for precaching. - * @param {string} [options.plugins] Plugins to use when precaching as well - * as responding to fetch events for precached assets. - * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to - * get the response from the network if there's a precache miss. - */ - constructor({ - cacheName, - plugins = [], - fallbackToNetwork = true - } = {}) { - this._urlsToCacheKeys = new Map(); - this._urlsToCacheModes = new Map(); - this._cacheKeysToIntegrities = new Map(); - this._strategy = new PrecacheStrategy({ - cacheName: cacheNames.getPrecacheName(cacheName), - plugins: [...plugins, new PrecacheCacheKeyPlugin({ - precacheController: this - })], - fallbackToNetwork - }); - // Bind the install and activate methods to the instance. - this.install = this.install.bind(this); - this.activate = this.activate.bind(this); - } - /** - * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and - * used to cache assets and respond to fetch events. - */ - get strategy() { - return this._strategy; - } - /** - * Adds items to the precache list, removing any duplicates and - * stores the files in the - * {@link workbox-core.cacheNames|"precache cache"} when the service - * worker installs. - * - * This method can be called multiple times. - * - * @param {Array} [entries=[]] Array of entries to precache. - */ - precache(entries) { - this.addToCacheList(entries); - if (!this._installAndActiveListenersAdded) { - self.addEventListener('install', this.install); - self.addEventListener('activate', this.activate); - this._installAndActiveListenersAdded = true; - } - } - /** - * This method will add items to the precache list, removing duplicates - * and ensuring the information is valid. - * - * @param {Array} entries - * Array of entries to precache. - */ - addToCacheList(entries) { - { - finalAssertExports.isArray(entries, { - moduleName: 'workbox-precaching', - className: 'PrecacheController', - funcName: 'addToCacheList', - paramName: 'entries' - }); - } - const urlsToWarnAbout = []; - for (const entry of entries) { - // See https://github.com/GoogleChrome/workbox/issues/2259 - if (typeof entry === 'string') { - urlsToWarnAbout.push(entry); - } else if (entry && entry.revision === undefined) { - urlsToWarnAbout.push(entry.url); - } - const { - cacheKey, - url - } = createCacheKey(entry); - const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default'; - if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { - throw new WorkboxError('add-to-cache-list-conflicting-entries', { - firstEntry: this._urlsToCacheKeys.get(url), - secondEntry: cacheKey + } catch (error) { + if (error instanceof Error) { + for (const callback of handler.iterateCallbacks("handlerDidError")) { + response = await callback({ + error, + event, + request }); - } - if (typeof entry !== 'string' && entry.integrity) { - if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) { - throw new WorkboxError('add-to-cache-list-conflicting-integrities', { - url - }); - } - this._cacheKeysToIntegrities.set(cacheKey, entry.integrity); - } - this._urlsToCacheKeys.set(url, cacheKey); - this._urlsToCacheModes.set(url, cacheMode); - if (urlsToWarnAbout.length > 0) { - const warningMessage = `Workbox is precaching URLs without revision ` + `info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` + `Learn more at https://bit.ly/wb-precache`; - { - logger.warn(warningMessage); + if (response) { + break; } } } + if (!response) { + throw error; + } else { + logger.log( + `While responding to '${getFriendlyURL(request.url)}', ` + + `an ${error instanceof Error ? error.toString() : ""} error occurred. Using a fallback response provided by ` + + `a handlerDidError plugin.` + ); + } } - /** - * Precaches new and updated assets. Call this method from the service worker - * install event. - * - * Note: this method calls `event.waitUntil()` for you, so you do not need - * to call it yourself in your event handlers. - * - * @param {ExtendableEvent} event - * @return {Promise} - */ - install(event) { - // waitUntil returns Promise - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return waitUntil(event, async () => { - const installReportPlugin = new PrecacheInstallReportPlugin(); - this.strategy.plugins.push(installReportPlugin); - // Cache entries one at a time. - // See https://github.com/GoogleChrome/workbox/issues/2528 - for (const [url, cacheKey] of this._urlsToCacheKeys) { - const integrity = this._cacheKeysToIntegrities.get(cacheKey); - const cacheMode = this._urlsToCacheModes.get(url); - const request = new Request(url, { - integrity, - cache: cacheMode, - credentials: 'same-origin' - }); - await Promise.all(this.strategy.handleAll({ - params: { - cacheKey - }, - request, - event - })); - } - const { - updatedURLs, - notUpdatedURLs - } = installReportPlugin; - { - printInstallDetails(updatedURLs, notUpdatedURLs); - } - return { - updatedURLs, - notUpdatedURLs - }; + for (const callback of handler.iterateCallbacks("handlerWillRespond")) { + response = await callback({ + event, + request, + response }); } - /** - * Deletes assets that are no longer present in the current precache manifest. - * Call this method from the service worker activate event. - * - * Note: this method calls `event.waitUntil()` for you, so you do not need - * to call it yourself in your event handlers. - * - * @param {ExtendableEvent} event - * @return {Promise} - */ - activate(event) { - // waitUntil returns Promise - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return waitUntil(event, async () => { - const cache = await self.caches.open(this.strategy.cacheName); - const currentlyCachedRequests = await cache.keys(); - const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); - const deletedURLs = []; - for (const request of currentlyCachedRequests) { - if (!expectedCacheKeys.has(request.url)) { - await cache.delete(request); - deletedURLs.push(request.url); - } - } - { - printCleanupDetails(deletedURLs); - } - return { - deletedURLs - }; + return response; + } + async _awaitComplete(responseDone, handler, request, event) { + let response; + let error; + try { + response = await responseDone; + } catch (error) { + // Ignore errors, as response errors should be caught via the `response` + // promise above. The `done` promise will only throw for errors in + // promises passed to `handler.waitUntil()`. + } + try { + await handler.runCallbacks("handlerDidRespond", { + event, + request, + response }); - } - /** - * Returns a mapping of a precached URL to the corresponding cache key, taking - * into account the revision information for the URL. - * - * @return {Map} A URL to cache key mapping. - */ - getURLsToCacheKeys() { - return this._urlsToCacheKeys; - } - /** - * Returns a list of all the URLs that have been precached by the current - * service worker. - * - * @return {Array} The precached URLs. - */ - getCachedURLs() { - return [...this._urlsToCacheKeys.keys()]; - } - /** - * Returns the cache key used for storing a given URL. If that URL is - * unversioned, like `/index.html', then the cache key will be the original - * URL with a search parameter appended to it. - * - * @param {string} url A URL whose cache key you want to look up. - * @return {string} The versioned URL that corresponds to a cache key - * for the original URL, or undefined if that URL isn't precached. - */ - getCacheKeyForURL(url) { - const urlObject = new URL(url, location.href); - return this._urlsToCacheKeys.get(urlObject.href); - } - /** - * @param {string} url A cache key whose SRI you want to look up. - * @return {string} The subresource integrity associated with the cache key, - * or undefined if it's not set. - */ - getIntegrityForCacheKey(cacheKey) { - return this._cacheKeysToIntegrities.get(cacheKey); - } - /** - * This acts as a drop-in replacement for - * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) - * with the following differences: - * - * - It knows what the name of the precache is, and only checks in that cache. - * - It allows you to pass in an "original" URL without versioning parameters, - * and it will automatically look up the correct cache key for the currently - * active revision of that URL. - * - * E.g., `matchPrecache('index.html')` will find the correct precached - * response for the currently active service worker, even if the actual cache - * key is `'/index.html?__WB_REVISION__=1234abcd'`. - * - * @param {string|Request} request The key (without revisioning parameters) - * to look up in the precache. - * @return {Promise} - */ - async matchPrecache(request) { - const url = request instanceof Request ? request.url : request; - const cacheKey = this.getCacheKeyForURL(url); - if (cacheKey) { - const cache = await self.caches.open(this.strategy.cacheName); - return cache.match(cacheKey); + await handler.doneWaiting(); + } catch (waitUntilError) { + if (waitUntilError instanceof Error) { + error = waitUntilError; } - return undefined; } - /** - * Returns a function that looks up `url` in the precache (taking into - * account revision information), and returns the corresponding `Response`. - * - * @param {string} url The precached URL which will be used to lookup the - * `Response`. - * @return {workbox-routing~handlerCallback} - */ - createHandlerBoundToURL(url) { - const cacheKey = this.getCacheKeyForURL(url); - if (!cacheKey) { - throw new WorkboxError('non-precached-url', { - url - }); - } - return options => { - options.request = new Request(url); - options.params = Object.assign({ - cacheKey - }, options.params); - return this.strategy.handle(options); - }; + await handler.runCallbacks("handlerDidComplete", { + event, + request, + response, + error: error + }); + handler.destroy(); + if (error) { + throw error; } } + } + /** + * Classes extending the `Strategy` based class should implement this method, + * and leverage the {@link workbox-strategies.StrategyHandler} + * arg to perform all fetching and cache logic, which will ensure all relevant + * cache, cache options, fetch options and plugins are used (per the current + * strategy instance). + * + * @name _handle + * @instance + * @abstract + * @function + * @param {Request} request + * @param {workbox-strategies.StrategyHandler} handler + * @return {Promise} + * + * @memberof workbox-strategies.Strategy + */ - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - let precacheController; - /** - * @return {PrecacheController} - * @private - */ - const getOrCreatePrecacheController = () => { - if (!precacheController) { - precacheController = new PrecacheController(); - } - return precacheController; - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Removes any URL search parameters that should be ignored. - * - * @param {URL} urlObject The original URL. - * @param {Array} ignoreURLParametersMatching RegExps to test against - * each search parameter name. Matches mean that the search parameter should be - * ignored. - * @return {URL} The URL with any ignored search parameters removed. - * - * @private - * @memberof workbox-precaching - */ - function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) { - // Convert the iterable into an array at the start of the loop to make sure - // deletion doesn't mess up iteration. - for (const paramName of [...urlObject.searchParams.keys()]) { - if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) { - urlObject.searchParams.delete(paramName); - } - } - return urlObject; - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Generator function that yields possible variations on the original URL to - * check, one at a time. - * - * @param {string} url - * @param {Object} options - * - * @private - * @memberof workbox-precaching - */ - function* generateURLVariations(url, { - ignoreURLParametersMatching = [/^utm_/, /^fbclid$/], - directoryIndex = 'index.html', - cleanURLs = true, - urlManipulation - } = {}) { - const urlObject = new URL(url, location.href); - urlObject.hash = ''; - yield urlObject.href; - const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); - yield urlWithoutIgnoredParams.href; - if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) { - const directoryURL = new URL(urlWithoutIgnoredParams.href); - directoryURL.pathname += directoryIndex; - yield directoryURL.href; - } - if (cleanURLs) { - const cleanURL = new URL(urlWithoutIgnoredParams.href); - cleanURL.pathname += '.html'; - yield cleanURL.href; - } - if (urlManipulation) { - const additionalURLs = urlManipulation({ - url: urlObject - }); - for (const urlToAttempt of additionalURLs) { - yield urlToAttempt.href; - } - } - } - - /* + /* Copyright 2020 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ + /** + * A {@link workbox-strategies.Strategy} implementation + * specifically designed to work with + * {@link workbox-precaching.PrecacheController} + * to both cache and fetch precached assets. + * + * Note: an instance of this class is created automatically when creating a + * `PrecacheController`; it's generally not necessary to create this yourself. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-precaching + */ + class PrecacheStrategy extends Strategy { /** - * A subclass of {@link workbox-routing.Route} that takes a - * {@link workbox-precaching.PrecacheController} - * instance and uses it to match incoming requests and handle fetching - * responses from the precache. * - * @memberof workbox-precaching - * @extends workbox-routing.Route + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} + * of all fetch() requests made by this strategy. + * @param {Object} [options.matchOptions] The + * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. */ - class PrecacheRoute extends Route { - /** - * @param {PrecacheController} precacheController A `PrecacheController` - * instance used to both match requests and respond to fetch events. - * @param {Object} [options] Options to control how requests are matched - * against the list of precached URLs. - * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will - * check cache entries for a URLs ending with '/' to see if there is a hit when - * appending the `directoryIndex` value. - * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An - * array of regex's to remove search params when looking for a cache match. - * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will - * check the cache for the URL with a `.html` added to the end of the end. - * @param {workbox-precaching~urlManipulation} [options.urlManipulation] - * This is a function that should take a URL and return an array of - * alternative URLs that should be checked for precache matches. - */ - constructor(precacheController, options) { - const match = ({ - request - }) => { - const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); - for (const possibleURL of generateURLVariations(request.url, options)) { - const cacheKey = urlsToCacheKeys.get(possibleURL); - if (cacheKey) { - const integrity = precacheController.getIntegrityForCacheKey(cacheKey); - return { - cacheKey, - integrity - }; + constructor(options = {}) { + options.cacheName = cacheNames.getPrecacheName(options.cacheName); + super(options); + this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true; + // Redirected responses cannot be used to satisfy a navigation request, so + // any redirected response must be "copied" rather than cloned, so the new + // response doesn't contain the `redirected` flag. See: + // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1 + this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin); + } + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const response = await handler.cacheMatch(request); + if (response) { + return response; + } + // If this is an `install` event for an entry that isn't already cached, + // then populate the cache. + if (handler.event && handler.event.type === "install") { + return await this._handleInstall(request, handler); + } + // Getting here means something went wrong. An entry that should have been + // precached wasn't found in the cache. + return await this._handleFetch(request, handler); + } + async _handleFetch(request, handler) { + let response; + const params = handler.params || {}; + // Fall back to the network if we're configured to do so. + if (this._fallbackToNetwork) { + { + logger.warn( + `The precached response for ` + + `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` + + `found. Falling back to the network.` + ); + } + const integrityInManifest = params.integrity; + const integrityInRequest = request.integrity; + const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest; + // Do not add integrity if the original request is no-cors + // See https://github.com/GoogleChrome/workbox/issues/3096 + response = await handler.fetch( + new Request(request, { + integrity: request.mode !== "no-cors" ? integrityInRequest || integrityInManifest : undefined + }) + ); + // It's only "safe" to repair the cache if we're using SRI to guarantee + // that the response matches the precache manifest's expectations, + // and there's either a) no integrity property in the incoming request + // or b) there is an integrity, and it matches the precache manifest. + // See https://github.com/GoogleChrome/workbox/issues/2858 + // Also if the original request users no-cors we don't use integrity. + // See https://github.com/GoogleChrome/workbox/issues/3096 + if (integrityInManifest && noIntegrityConflict && request.mode !== "no-cors") { + this._useDefaultCacheabilityPluginIfNeeded(); + const wasCached = await handler.cachePut(request, response.clone()); + { + if (wasCached) { + logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`); } } - { - logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url)); - } - return; - }; - super(match, precacheController.strategy); + } + } else { + // This shouldn't normally happen, but there are edge cases: + // https://github.com/GoogleChrome/workbox/issues/1441 + throw new WorkboxError("missing-precache-entry", { + cacheName: this.cacheName, + url: request.url + }); } + { + const cacheKey = params.cacheKey || (await handler.getCacheKey(request, "read")); + // Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url)); + logger.log( + `Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}` + ); + logger.groupCollapsed(`View request details here.`); + logger.log(request); + logger.groupEnd(); + logger.groupCollapsed(`View response details here.`); + logger.log(response); + logger.groupEnd(); + logger.groupEnd(); + } + return response; + } + async _handleInstall(request, handler) { + this._useDefaultCacheabilityPluginIfNeeded(); + const response = await handler.fetch(request); + // Make sure we defer cachePut() until after we know the response + // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737 + const wasCached = await handler.cachePut(request, response.clone()); + if (!wasCached) { + // Throwing here will lead to the `install` handler failing, which + // we want to do if *any* of the responses aren't safe to cache. + throw new WorkboxError("bad-precaching-response", { + url: request.url, + status: response.status + }); + } + return response; } - - /* - Copyright 2019 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ /** - * Add a `fetch` listener to the service worker that will - * respond to - * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} - * with precached assets. + * This method is complex, as there a number of things to account for: * - * Requests for assets that aren't precached, the `FetchEvent` will not be - * responded to, allowing the event to fall through to other `fetch` event - * listeners. + * The `plugins` array can be set at construction, and/or it might be added to + * to at any time before the strategy is used. * - * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute} - * options. + * At the time the strategy is used (i.e. during an `install` event), there + * needs to be at least one plugin that implements `cacheWillUpdate` in the + * array, other than `copyRedirectedCacheableResponsesPlugin`. * - * @memberof workbox-precaching + * - If this method is called and there are no suitable `cacheWillUpdate` + * plugins, we need to add `defaultPrecacheCacheabilityPlugin`. + * + * - If this method is called and there is exactly one `cacheWillUpdate`, then + * we don't have to do anything (this might be a previously added + * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin). + * + * - If this method is called and there is more than one `cacheWillUpdate`, + * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so, + * we need to remove it. (This situation is unlikely, but it could happen if + * the strategy is used multiple times, the first without a `cacheWillUpdate`, + * and then later on after manually adding a custom `cacheWillUpdate`.) + * + * See https://github.com/GoogleChrome/workbox/issues/2737 for more context. + * + * @private */ - function addRoute(options) { - const precacheController = getOrCreatePrecacheController(); - const precacheRoute = new PrecacheRoute(precacheController, options); - registerRoute(precacheRoute); + _useDefaultCacheabilityPluginIfNeeded() { + let defaultPluginIndex = null; + let cacheWillUpdatePluginCount = 0; + for (const [index, plugin] of this.plugins.entries()) { + // Ignore the copy redirected plugin when determining what to do. + if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) { + continue; + } + // Save the default plugin's index, in case it needs to be removed. + if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) { + defaultPluginIndex = index; + } + if (plugin.cacheWillUpdate) { + cacheWillUpdatePluginCount++; + } + } + if (cacheWillUpdatePluginCount === 0) { + this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin); + } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) { + // Only remove the default plugin; multiple custom plugins are allowed. + this.plugins.splice(defaultPluginIndex, 1); + } + // Nothing needs to be done if cacheWillUpdatePluginCount is 1 } + } + PrecacheStrategy.defaultPrecacheCacheabilityPlugin = { + async cacheWillUpdate({ response }) { + if (!response || response.status >= 400) { + return null; + } + return response; + } + }; + PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = { + async cacheWillUpdate({ response }) { + return response.redirected ? await copyResponse(response) : response; + } + }; - /* + /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ + /** + * Performs efficient precaching of assets. + * + * @memberof workbox-precaching + */ + class PrecacheController { + /** + * Create a new PrecacheController. + * + * @param {Object} [options] + * @param {string} [options.cacheName] The cache to use for precaching. + * @param {string} [options.plugins] Plugins to use when precaching as well + * as responding to fetch events for precached assets. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. + */ + constructor({ cacheName, plugins = [], fallbackToNetwork = true } = {}) { + this._urlsToCacheKeys = new Map(); + this._urlsToCacheModes = new Map(); + this._cacheKeysToIntegrities = new Map(); + this._strategy = new PrecacheStrategy({ + cacheName: cacheNames.getPrecacheName(cacheName), + plugins: [ + ...plugins, + new PrecacheCacheKeyPlugin({ + precacheController: this + }) + ], + fallbackToNetwork + }); + // Bind the install and activate methods to the instance. + this.install = this.install.bind(this); + this.activate = this.activate.bind(this); + } + /** + * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and + * used to cache assets and respond to fetch events. + */ + get strategy() { + return this._strategy; + } /** * Adds items to the precache list, removing any duplicates and * stores the files in the @@ -3140,252 +2742,694 @@ define(['exports'], (function (exports) { 'use strict'; * * This method can be called multiple times. * - * Please note: This method **will not** serve any of the cached files for you. - * It only precaches files. To respond to a network request you call - * {@link workbox-precaching.addRoute}. - * - * If you have a single array of files to precache, you can just call - * {@link workbox-precaching.precacheAndRoute}. - * * @param {Array} [entries=[]] Array of entries to precache. - * - * @memberof workbox-precaching */ - function precache(entries) { - const precacheController = getOrCreatePrecacheController(); - precacheController.precache(entries); + precache(entries) { + this.addToCacheList(entries); + if (!this._installAndActiveListenersAdded) { + self.addEventListener("install", this.install); + self.addEventListener("activate", this.activate); + this._installAndActiveListenersAdded = true; + } } + /** + * This method will add items to the precache list, removing duplicates + * and ensuring the information is valid. + * + * @param {Array} entries + * Array of entries to precache. + */ + addToCacheList(entries) { + { + finalAssertExports.isArray(entries, { + moduleName: "workbox-precaching", + className: "PrecacheController", + funcName: "addToCacheList", + paramName: "entries" + }); + } + const urlsToWarnAbout = []; + for (const entry of entries) { + // See https://github.com/GoogleChrome/workbox/issues/2259 + if (typeof entry === "string") { + urlsToWarnAbout.push(entry); + } else if (entry && entry.revision === undefined) { + urlsToWarnAbout.push(entry.url); + } + const { cacheKey, url } = createCacheKey(entry); + const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default"; + if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { + throw new WorkboxError("add-to-cache-list-conflicting-entries", { + firstEntry: this._urlsToCacheKeys.get(url), + secondEntry: cacheKey + }); + } + if (typeof entry !== "string" && entry.integrity) { + if ( + this._cacheKeysToIntegrities.has(cacheKey) && + this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity + ) { + throw new WorkboxError("add-to-cache-list-conflicting-integrities", { + url + }); + } + this._cacheKeysToIntegrities.set(cacheKey, entry.integrity); + } + this._urlsToCacheKeys.set(url, cacheKey); + this._urlsToCacheModes.set(url, cacheMode); + if (urlsToWarnAbout.length > 0) { + const warningMessage = + `Workbox is precaching URLs without revision ` + + `info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe. ` + + `Learn more at https://bit.ly/wb-precache`; + { + logger.warn(warningMessage); + } + } + } + } + /** + * Precaches new and updated assets. Call this method from the service worker + * install event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + install(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const installReportPlugin = new PrecacheInstallReportPlugin(); + this.strategy.plugins.push(installReportPlugin); + // Cache entries one at a time. + // See https://github.com/GoogleChrome/workbox/issues/2528 + for (const [url, cacheKey] of this._urlsToCacheKeys) { + const integrity = this._cacheKeysToIntegrities.get(cacheKey); + const cacheMode = this._urlsToCacheModes.get(url); + const request = new Request(url, { + integrity, + cache: cacheMode, + credentials: "same-origin" + }); + await Promise.all( + this.strategy.handleAll({ + params: { + cacheKey + }, + request, + event + }) + ); + } + const { updatedURLs, notUpdatedURLs } = installReportPlugin; + { + printInstallDetails(updatedURLs, notUpdatedURLs); + } + return { + updatedURLs, + notUpdatedURLs + }; + }); + } + /** + * Deletes assets that are no longer present in the current precache manifest. + * Call this method from the service worker activate event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + activate(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const cache = await self.caches.open(this.strategy.cacheName); + const currentlyCachedRequests = await cache.keys(); + const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); + const deletedURLs = []; + for (const request of currentlyCachedRequests) { + if (!expectedCacheKeys.has(request.url)) { + await cache.delete(request); + deletedURLs.push(request.url); + } + } + { + printCleanupDetails(deletedURLs); + } + return { + deletedURLs + }; + }); + } + /** + * Returns a mapping of a precached URL to the corresponding cache key, taking + * into account the revision information for the URL. + * + * @return {Map} A URL to cache key mapping. + */ + getURLsToCacheKeys() { + return this._urlsToCacheKeys; + } + /** + * Returns a list of all the URLs that have been precached by the current + * service worker. + * + * @return {Array} The precached URLs. + */ + getCachedURLs() { + return [...this._urlsToCacheKeys.keys()]; + } + /** + * Returns the cache key used for storing a given URL. If that URL is + * unversioned, like `/index.html', then the cache key will be the original + * URL with a search parameter appended to it. + * + * @param {string} url A URL whose cache key you want to look up. + * @return {string} The versioned URL that corresponds to a cache key + * for the original URL, or undefined if that URL isn't precached. + */ + getCacheKeyForURL(url) { + const urlObject = new URL(url, location.href); + return this._urlsToCacheKeys.get(urlObject.href); + } + /** + * @param {string} url A cache key whose SRI you want to look up. + * @return {string} The subresource integrity associated with the cache key, + * or undefined if it's not set. + */ + getIntegrityForCacheKey(cacheKey) { + return this._cacheKeysToIntegrities.get(cacheKey); + } + /** + * This acts as a drop-in replacement for + * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) + * with the following differences: + * + * - It knows what the name of the precache is, and only checks in that cache. + * - It allows you to pass in an "original" URL without versioning parameters, + * and it will automatically look up the correct cache key for the currently + * active revision of that URL. + * + * E.g., `matchPrecache('index.html')` will find the correct precached + * response for the currently active service worker, even if the actual cache + * key is `'/index.html?__WB_REVISION__=1234abcd'`. + * + * @param {string|Request} request The key (without revisioning parameters) + * to look up in the precache. + * @return {Promise} + */ + async matchPrecache(request) { + const url = request instanceof Request ? request.url : request; + const cacheKey = this.getCacheKeyForURL(url); + if (cacheKey) { + const cache = await self.caches.open(this.strategy.cacheName); + return cache.match(cacheKey); + } + return undefined; + } + /** + * Returns a function that looks up `url` in the precache (taking into + * account revision information), and returns the corresponding `Response`. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @return {workbox-routing~handlerCallback} + */ + createHandlerBoundToURL(url) { + const cacheKey = this.getCacheKeyForURL(url); + if (!cacheKey) { + throw new WorkboxError("non-precached-url", { + url + }); + } + return (options) => { + options.request = new Request(url); + options.params = Object.assign( + { + cacheKey + }, + options.params + ); + return this.strategy.handle(options); + }; + } + } - /* + /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - /** - * This method will add entries to the precache list and add a route to - * respond to fetch events. - * - * This is a convenience method that will call - * {@link workbox-precaching.precache} and - * {@link workbox-precaching.addRoute} in a single call. - * - * @param {Array} entries Array of entries to precache. - * @param {Object} [options] See the - * {@link workbox-precaching.PrecacheRoute} options. - * - * @memberof workbox-precaching - */ - function precacheAndRoute(entries, options) { - precache(entries); - addRoute(options); + let precacheController; + /** + * @return {PrecacheController} + * @private + */ + const getOrCreatePrecacheController = () => { + if (!precacheController) { + precacheController = new PrecacheController(); } + return precacheController; + }; - /* + /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - const SUBSTRING_TO_FIND = '-precache-'; - /** - * Cleans up incompatible precaches that were created by older versions of - * Workbox, by a service worker registered under the current scope. - * - * This is meant to be called as part of the `activate` event. - * - * This should be safe to use as long as you don't include `substringToFind` - * (defaulting to `-precache-`) in your non-precache cache names. - * - * @param {string} currentPrecacheName The cache name currently in use for - * precaching. This cache won't be deleted. - * @param {string} [substringToFind='-precache-'] Cache names which include this - * substring will be deleted (excluding `currentPrecacheName`). - * @return {Array} A list of all the cache names that were deleted. - * - * @private - * @memberof workbox-precaching - */ - const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { - const cacheNames = await self.caches.keys(); - const cacheNamesToDelete = cacheNames.filter(cacheName => { - return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName; - }); - await Promise.all(cacheNamesToDelete.map(cacheName => self.caches.delete(cacheName))); - return cacheNamesToDelete; - }; + /** + * Removes any URL search parameters that should be ignored. + * + * @param {URL} urlObject The original URL. + * @param {Array} ignoreURLParametersMatching RegExps to test against + * each search parameter name. Matches mean that the search parameter should be + * ignored. + * @return {URL} The URL with any ignored search parameters removed. + * + * @private + * @memberof workbox-precaching + */ + function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) { + // Convert the iterable into an array at the start of the loop to make sure + // deletion doesn't mess up iteration. + for (const paramName of [...urlObject.searchParams.keys()]) { + if (ignoreURLParametersMatching.some((regExp) => regExp.test(paramName))) { + urlObject.searchParams.delete(paramName); + } + } + return urlObject; + } - /* + /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ + /** + * Generator function that yields possible variations on the original URL to + * check, one at a time. + * + * @param {string} url + * @param {Object} options + * + * @private + * @memberof workbox-precaching + */ + function* generateURLVariations( + url, + { + ignoreURLParametersMatching = [/^utm_/, /^fbclid$/], + directoryIndex = "index.html", + cleanURLs = true, + urlManipulation + } = {} + ) { + const urlObject = new URL(url, location.href); + urlObject.hash = ""; + yield urlObject.href; + const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); + yield urlWithoutIgnoredParams.href; + if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith("/")) { + const directoryURL = new URL(urlWithoutIgnoredParams.href); + directoryURL.pathname += directoryIndex; + yield directoryURL.href; + } + if (cleanURLs) { + const cleanURL = new URL(urlWithoutIgnoredParams.href); + cleanURL.pathname += ".html"; + yield cleanURL.href; + } + if (urlManipulation) { + const additionalURLs = urlManipulation({ + url: urlObject + }); + for (const urlToAttempt of additionalURLs) { + yield urlToAttempt.href; + } + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A subclass of {@link workbox-routing.Route} that takes a + * {@link workbox-precaching.PrecacheController} + * instance and uses it to match incoming requests and handle fetching + * responses from the precache. + * + * @memberof workbox-precaching + * @extends workbox-routing.Route + */ + class PrecacheRoute extends Route { /** - * Adds an `activate` event listener which will clean up incompatible - * precaches that were created by older versions of Workbox. - * - * @memberof workbox-precaching + * @param {PrecacheController} precacheController A `PrecacheController` + * instance used to both match requests and respond to fetch events. + * @param {Object} [options] Options to control how requests are matched + * against the list of precached URLs. + * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will + * check cache entries for a URLs ending with '/' to see if there is a hit when + * appending the `directoryIndex` value. + * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An + * array of regex's to remove search params when looking for a cache match. + * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will + * check the cache for the URL with a `.html` added to the end of the end. + * @param {workbox-precaching~urlManipulation} [options.urlManipulation] + * This is a function that should take a URL and return an array of + * alternative URLs that should be checked for precache matches. */ - function cleanupOutdatedCaches() { - // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 - self.addEventListener('activate', event => { - const cacheName = cacheNames.getPrecacheName(); - event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => { + constructor(precacheController, options) { + const match = ({ request }) => { + const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); + for (const possibleURL of generateURLVariations(request.url, options)) { + const cacheKey = urlsToCacheKeys.get(possibleURL); + if (cacheKey) { + const integrity = precacheController.getIntegrityForCacheKey(cacheKey); + return { + cacheKey, + integrity + }; + } + } + { + logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url)); + } + return; + }; + super(match, precacheController.strategy); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Add a `fetch` listener to the service worker that will + * respond to + * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} + * with precached assets. + * + * Requests for assets that aren't precached, the `FetchEvent` will not be + * responded to, allowing the event to fall through to other `fetch` event + * listeners. + * + * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute} + * options. + * + * @memberof workbox-precaching + */ + function addRoute(options) { + const precacheController = getOrCreatePrecacheController(); + const precacheRoute = new PrecacheRoute(precacheController, options); + registerRoute(precacheRoute); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * {@link workbox-core.cacheNames|"precache cache"} when the service + * worker installs. + * + * This method can be called multiple times. + * + * Please note: This method **will not** serve any of the cached files for you. + * It only precaches files. To respond to a network request you call + * {@link workbox-precaching.addRoute}. + * + * If you have a single array of files to precache, you can just call + * {@link workbox-precaching.precacheAndRoute}. + * + * @param {Array} [entries=[]] Array of entries to precache. + * + * @memberof workbox-precaching + */ + function precache(entries) { + const precacheController = getOrCreatePrecacheController(); + precacheController.precache(entries); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This method will add entries to the precache list and add a route to + * respond to fetch events. + * + * This is a convenience method that will call + * {@link workbox-precaching.precache} and + * {@link workbox-precaching.addRoute} in a single call. + * + * @param {Array} entries Array of entries to precache. + * @param {Object} [options] See the + * {@link workbox-precaching.PrecacheRoute} options. + * + * @memberof workbox-precaching + */ + function precacheAndRoute(entries, options) { + precache(entries); + addRoute(options); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const SUBSTRING_TO_FIND = "-precache-"; + /** + * Cleans up incompatible precaches that were created by older versions of + * Workbox, by a service worker registered under the current scope. + * + * This is meant to be called as part of the `activate` event. + * + * This should be safe to use as long as you don't include `substringToFind` + * (defaulting to `-precache-`) in your non-precache cache names. + * + * @param {string} currentPrecacheName The cache name currently in use for + * precaching. This cache won't be deleted. + * @param {string} [substringToFind='-precache-'] Cache names which include this + * substring will be deleted (excluding `currentPrecacheName`). + * @return {Array} A list of all the cache names that were deleted. + * + * @private + * @memberof workbox-precaching + */ + const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { + const cacheNames = await self.caches.keys(); + const cacheNamesToDelete = cacheNames.filter((cacheName) => { + return ( + cacheName.includes(substringToFind) && + cacheName.includes(self.registration.scope) && + cacheName !== currentPrecacheName + ); + }); + await Promise.all(cacheNamesToDelete.map((cacheName) => self.caches.delete(cacheName))); + return cacheNamesToDelete; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds an `activate` event listener which will clean up incompatible + * precaches that were created by older versions of Workbox. + * + * @memberof workbox-precaching + */ + function cleanupOutdatedCaches() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener("activate", (event) => { + const cacheName = cacheNames.getPrecacheName(); + event.waitUntil( + deleteOutdatedCaches(cacheName).then((cachesDeleted) => { { if (cachesDeleted.length > 0) { logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted); } } - })); - }); - } + }) + ); + }); + } - /* + /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ + /** + * NavigationRoute makes it easy to create a + * {@link workbox-routing.Route} that matches for browser + * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. + * + * It will only match incoming Requests whose + * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode} + * is set to `navigate`. + * + * You can optionally only apply this route to a subset of navigation requests + * by using one or both of the `denylist` and `allowlist` parameters. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class NavigationRoute extends Route { /** - * NavigationRoute makes it easy to create a - * {@link workbox-routing.Route} that matches for browser - * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. + * If both `denylist` and `allowlist` are provided, the `denylist` will + * take precedence and the request will not match this route. * - * It will only match incoming Requests whose - * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode} - * is set to `navigate`. + * The regular expressions in `allowlist` and `denylist` + * are matched against the concatenated + * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} + * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} + * portions of the requested URL. * - * You can optionally only apply this route to a subset of navigation requests - * by using one or both of the `denylist` and `allowlist` parameters. + * *Note*: These RegExps may be evaluated against every destination URL during + * a navigation. Avoid using + * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077), + * or else your users may see delays when navigating your site. * - * @memberof workbox-routing - * @extends workbox-routing.Route + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {Object} options + * @param {Array} [options.denylist] If any of these patterns match, + * the route will not handle the request (even if a allowlist RegExp matches). + * @param {Array} [options.allowlist=[/./]] If any of these patterns + * match the URL's pathname and search parameter, the route will handle the + * request (assuming the denylist doesn't match). */ - class NavigationRoute extends Route { - /** - * If both `denylist` and `allowlist` are provided, the `denylist` will - * take precedence and the request will not match this route. - * - * The regular expressions in `allowlist` and `denylist` - * are matched against the concatenated - * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} - * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} - * portions of the requested URL. - * - * *Note*: These RegExps may be evaluated against every destination URL during - * a navigation. Avoid using - * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077), - * or else your users may see delays when navigating your site. - * - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - * @param {Object} options - * @param {Array} [options.denylist] If any of these patterns match, - * the route will not handle the request (even if a allowlist RegExp matches). - * @param {Array} [options.allowlist=[/./]] If any of these patterns - * match the URL's pathname and search parameter, the route will handle the - * request (assuming the denylist doesn't match). - */ - constructor(handler, { - allowlist = [/./], - denylist = [] - } = {}) { - { - finalAssertExports.isArrayOfClass(allowlist, RegExp, { - moduleName: 'workbox-routing', - className: 'NavigationRoute', - funcName: 'constructor', - paramName: 'options.allowlist' - }); - finalAssertExports.isArrayOfClass(denylist, RegExp, { - moduleName: 'workbox-routing', - className: 'NavigationRoute', - funcName: 'constructor', - paramName: 'options.denylist' - }); - } - super(options => this._match(options), handler); - this._allowlist = allowlist; - this._denylist = denylist; + constructor(handler, { allowlist = [/./], denylist = [] } = {}) { + { + finalAssertExports.isArrayOfClass(allowlist, RegExp, { + moduleName: "workbox-routing", + className: "NavigationRoute", + funcName: "constructor", + paramName: "options.allowlist" + }); + finalAssertExports.isArrayOfClass(denylist, RegExp, { + moduleName: "workbox-routing", + className: "NavigationRoute", + funcName: "constructor", + paramName: "options.denylist" + }); } - /** - * Routes match handler. - * - * @param {Object} options - * @param {URL} options.url - * @param {Request} options.request - * @return {boolean} - * - * @private - */ - _match({ - url, - request - }) { - if (request && request.mode !== 'navigate') { - return false; - } - const pathnameAndSearch = url.pathname + url.search; - for (const regExp of this._denylist) { - if (regExp.test(pathnameAndSearch)) { - { - logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp.toString()}`); - } - return false; - } - } - if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) { - { - logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`); - } - return true; - } - { - logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`); - } + super((options) => this._match(options), handler); + this._allowlist = allowlist; + this._denylist = denylist; + } + /** + * Routes match handler. + * + * @param {Object} options + * @param {URL} options.url + * @param {Request} options.request + * @return {boolean} + * + * @private + */ + _match({ url, request }) { + if (request && request.mode !== "navigate") { return false; } + const pathnameAndSearch = url.pathname + url.search; + for (const regExp of this._denylist) { + if (regExp.test(pathnameAndSearch)) { + { + logger.log( + `The navigation route ${pathnameAndSearch} is not ` + + `being used, since the URL matches this denylist pattern: ` + + `${regExp.toString()}` + ); + } + return false; + } + } + if (this._allowlist.some((regExp) => regExp.test(pathnameAndSearch))) { + { + logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`); + } + return true; + } + { + logger.log( + `The navigation route ${pathnameAndSearch} is not ` + + `being used, since the URL being navigated to doesn't ` + + `match the allowlist.` + ); + } + return false; } + } - /* + /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ - /** - * Helper function that calls - * {@link PrecacheController#createHandlerBoundToURL} on the default - * {@link PrecacheController} instance. - * - * If you are creating your own {@link PrecacheController}, then call the - * {@link PrecacheController#createHandlerBoundToURL} on that instance, - * instead of using this function. - * - * @param {string} url The precached URL which will be used to lookup the - * `Response`. - * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the - * response from the network if there's a precache miss. - * @return {workbox-routing~handlerCallback} - * - * @memberof workbox-precaching - */ - function createHandlerBoundToURL(url) { - const precacheController = getOrCreatePrecacheController(); - return precacheController.createHandlerBoundToURL(url); - } + /** + * Helper function that calls + * {@link PrecacheController#createHandlerBoundToURL} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call the + * {@link PrecacheController#createHandlerBoundToURL} on that instance, + * instead of using this function. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {workbox-routing~handlerCallback} + * + * @memberof workbox-precaching + */ + function createHandlerBoundToURL(url) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.createHandlerBoundToURL(url); + } - exports.NavigationRoute = NavigationRoute; - exports.cleanupOutdatedCaches = cleanupOutdatedCaches; - exports.clientsClaim = clientsClaim; - exports.createHandlerBoundToURL = createHandlerBoundToURL; - exports.precacheAndRoute = precacheAndRoute; - exports.registerRoute = registerRoute; - -})); + exports.NavigationRoute = NavigationRoute; + exports.cleanupOutdatedCaches = cleanupOutdatedCaches; + exports.clientsClaim = clientsClaim; + exports.createHandlerBoundToURL = createHandlerBoundToURL; + exports.precacheAndRoute = precacheAndRoute; + exports.registerRoute = registerRoute; +}); diff --git a/client/package-lock.json b/client/package-lock.json index 5fc873078..13a72ca36 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -85,7 +85,7 @@ "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.23.3", - "@dotenvx/dotenvx": "^0.15.0", + "@dotenvx/dotenvx": "^0.15.4", "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.3", "@sentry/webpack-plugin": "^2.14.2", @@ -111,6 +111,9 @@ }, "engines": { "node": "18.18.2" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.6.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2512,8 +2515,9 @@ }, "node_modules/@dotenvx/dotenvx": { "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-0.15.4.tgz", + "integrity": "sha512-3DBVbsdowmbM+MtjECtUSV/lmoSVSOZtGw6f5wHeeW5t3dwXYyAk8vRBayS16zBmk4WBRRlOpoowA5Wmk08MIg==", "dev": true, - "license": "MIT", "dependencies": { "@inquirer/prompts": "^3.3.0", "chalk": "^4.1.2", @@ -5671,6 +5675,161 @@ "node": ">= 8.0.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", + "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.12.0", "cpu": [ @@ -23242,6 +23401,19 @@ "node": ">=8" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-async": { "version": "3.0.0", "dev": true, diff --git a/client/package.json b/client/package.json index 135f59420..686a6c6e4 100644 --- a/client/package.json +++ b/client/package.json @@ -129,7 +129,7 @@ "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.23.3", - "@dotenvx/dotenvx": "^0.15.0", + "@dotenvx/dotenvx": "^0.15.4", "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.3", "@sentry/webpack-plugin": "^2.14.2", diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index 13c751c58..e4df37c61 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -1,59 +1,58 @@ -import {ApolloProvider} from "@apollo/client"; -import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react'; -import {ConfigProvider} from "antd"; +import { ApolloProvider } from "@apollo/client"; +import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react"; +import { ConfigProvider } from "antd"; import enLocale from "antd/es/locale/en_US"; import dayjs from "../utils/day"; -import 'dayjs/locale/en'; +import "dayjs/locale/en"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import client from "../utils/GraphQLClient"; import App from "./App"; import * as Sentry from "@sentry/react"; import themeProvider from "./themeProvider"; -import { Userpilot } from 'userpilot' +import { Userpilot } from "userpilot"; // Initialize Userpilot -if(import.meta.env.DEV){ - Userpilot.initialize('NX-69145f08'); +if (import.meta.env.DEV) { + Userpilot.initialize("NX-69145f08"); } dayjs.locale("en"); const config = { - core: { - authorizationKey: import.meta.env.VITE_APP_SPLIT_API, - key: "anon", - }, + core: { + authorizationKey: import.meta.env.VITE_APP_SPLIT_API, + key: "anon" + } }; export const factory = SplitSdk(config); - function AppContainer() { - const {t} = useTranslation(); + const { t } = useTranslation(); - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); } export default Sentry.withProfiler(AppContainer); diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 5453a25a3..50bde585a 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -17,242 +17,225 @@ import TechPageContainer from "../pages/tech/tech.page.container"; import { setOnline } from "../redux/application/application.actions"; import { selectOnline } from "../redux/application/application.selectors"; import { checkUserSession } from "../redux/user/user.actions"; -import { - selectBodyshop, - selectCurrentEula, - selectCurrentUser, -} from "../redux/user/user.selectors"; +import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors"; import PrivateRoute from "../components/PrivateRoute"; import "./App.styles.scss"; import handleBeta from "../utils/betaHandler"; import Eula from "../components/eula/eula.component"; import InstanceRenderMgr from "../utils/instanceRenderMgr"; -import { ProductFruits } from 'react-product-fruits'; +import { ProductFruits } from "react-product-fruits"; -const ResetPassword = lazy(() => - import("../pages/reset-password/reset-password.component") -); +const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); -const MobilePaymentContainer = lazy(() => - import("../pages/mobile-payment/mobile-payment.container") -); +const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container")); const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - online: selectOnline, - bodyshop: selectBodyshop, - currentEula: selectCurrentEula, + currentUser: selectCurrentUser, + online: selectOnline, + bodyshop: selectBodyshop, + currentEula: selectCurrentEula }); const mapDispatchToProps = (dispatch) => ({ - checkUserSession: () => dispatch(checkUserSession()), - setOnline: (isOnline) => dispatch(setOnline(isOnline)), + checkUserSession: () => dispatch(checkUserSession()), + setOnline: (isOnline) => dispatch(setOnline(isOnline)) }); -export function App({ - bodyshop, - checkUserSession, - currentUser, - online, - setOnline, - currentEula, -}) { - const client = useSplitClient().client; - const [listenersAdded, setListenersAdded] = useState(false); - const { t } = useTranslation(); +export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) { + const client = useSplitClient().client; + const [listenersAdded, setListenersAdded] = useState(false); + const { t } = useTranslation(); - useEffect(() => { - if (!navigator.onLine) { - setOnline(false); - } - - checkUserSession(); - }, [checkUserSession, setOnline]); - - //const b = Grid.useBreakpoint(); - // console.log("Breakpoints:", b); - - // Associate event listeners, memoize to prevent multiple listeners being added - useEffect(() => { - const offlineListener = (e) => { - setOnline(false); - }; - - const onlineListener = (e) => { - setOnline(true); - }; - - if (!listenersAdded) { - console.log("Added events for offline and online"); - window.addEventListener("offline", offlineListener); - window.addEventListener("online", onlineListener); - setListenersAdded(true); - } - - return () => { - window.removeEventListener("offline", offlineListener); - window.removeEventListener("online", onlineListener); - }; - }, [setOnline, listenersAdded]); - - useEffect(() => { - if (currentUser.authorized && bodyshop) { - client.setAttribute("imexshopid", bodyshop.imexshopid); - - if ( - client.getTreatment("LogRocket_Tracking") === "on" || - window.location.hostname === - InstanceRenderMgr({ - imex: "beta.imex.online", - rome: "beta.romeonline.io", - }) - ) { - console.log("LR Start"); - LogRocket.init( - InstanceRenderMgr({ - imex: "gvfvfw/bodyshopapp", - rome: "rome-online/rome-online", - promanager: "", //TODO:AIO Add in log rocket for promanager instances. - }) - ); - } - } - }, [bodyshop, client, currentUser.authorized]); - - if (currentUser.authorized === null) { - return ; + useEffect(() => { + if (!navigator.onLine) { + setOnline(false); } - handleBeta(); + checkUserSession(); + }, [checkUserSession, setOnline]); - if (!online) - return ( - { - window.location.reload(); - }} - > - {t("general.actions.refresh")} - - } - /> + //const b = Grid.useBreakpoint(); + // console.log("Breakpoints:", b); + + // Associate event listeners, memoize to prevent multiple listeners being added + useEffect(() => { + const offlineListener = (e) => { + setOnline(false); + }; + + const onlineListener = (e) => { + setOnline(true); + }; + + if (!listenersAdded) { + console.log("Added events for offline and online"); + window.addEventListener("offline", offlineListener); + window.addEventListener("online", onlineListener); + setListenersAdded(true); + } + + return () => { + window.removeEventListener("offline", offlineListener); + window.removeEventListener("online", onlineListener); + }; + }, [setOnline, listenersAdded]); + + useEffect(() => { + if (currentUser.authorized && bodyshop) { + client.setAttribute("imexshopid", bodyshop.imexshopid); + + if ( + client.getTreatment("LogRocket_Tracking") === "on" || + window.location.hostname === + InstanceRenderMgr({ + imex: "beta.imex.online", + rome: "beta.romeonline.io" + }) + ) { + console.log("LR Start"); + LogRocket.init( + InstanceRenderMgr({ + imex: "gvfvfw/bodyshopapp", + rome: "rome-online/rome-online", + promanager: "" //TODO:AIO Add in log rocket for promanager instances. + }) ); - - if (currentEula && !currentUser.eulaIsAccepted) { - return ; + } } + }, [bodyshop, client, currentUser.authorized]); - // Any route that is not assigned and matched will default to the Landing Page component + if (currentUser.authorized === null) { + return ; + } + + handleBeta(); + + if (!online) return ( - - - } - > - - - - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - > - } /> - - - - - } - > - } /> - - - } - > - } /> - - - + { + window.location.reload(); + }} + > + {t("general.actions.refresh")} + + } + /> ); + + if (currentEula && !currentUser.eulaIsAccepted) { + return ; + } + + // Any route that is not assigned and matched will default to the Landing Page component + return ( + + } + > + + + + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + > + } /> + + + + + } + > + } /> + + }> + } /> + + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 047cf2541..dd79912c1 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -154,7 +154,6 @@ font-style: italic; } - .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { background-color: #f4f4f4; } diff --git a/client/src/App/themeProvider.js b/client/src/App/themeProvider.js index f8947ccc0..18bcaaa1c 100644 --- a/client/src/App/themeProvider.js +++ b/client/src/App/themeProvider.js @@ -1,8 +1,8 @@ -import {defaultsDeep} from "lodash"; -import {theme} from "antd"; -import InstanceRenderMgr from '../utils/instanceRenderMgr' +import { defaultsDeep } from "lodash"; +import { theme } from "antd"; +import InstanceRenderMgr from "../utils/instanceRenderMgr"; -const {defaultAlgorithm, darkAlgorithm} = theme; +const { defaultAlgorithm, darkAlgorithm } = theme; let isDarkMode = false; @@ -13,28 +13,28 @@ let isDarkMode = false; const defaultTheme = { components: { Table: { - rowHoverBg: '#e7f3ff', - rowSelectedBg: '#e6f7ff', - headerSortHoverBg: 'transparent', + rowHoverBg: "#e7f3ff", + rowSelectedBg: "#e6f7ff", + headerSortHoverBg: "transparent" }, Menu: { - darkItemHoverBg: '#1890ff', - itemHoverBg: '#1890ff', - horizontalItemHoverBg: '#1890ff', - }, + darkItemHoverBg: "#1890ff", + itemHoverBg: "#1890ff", + horizontalItemHoverBg: "#1890ff" + } }, token: { - colorPrimary: InstanceRenderMgr({ - imex: '#1890ff', - rome: '#326ade', - promanager:"#1d69a6" + colorPrimary: InstanceRenderMgr({ + imex: "#1890ff", + rome: "#326ade", + promanager: "#1d69a6" }), colorInfo: InstanceRenderMgr({ - imex: '#1890ff', - rome: '#326ade', - promanager:"#1d69a6" - }), - }, + imex: "#1890ff", + rome: "#326ade", + promanager: "#1d69a6" + }) + } }; /** @@ -42,16 +42,16 @@ const defaultTheme = { * @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}} */ const devTheme = { - components: { - Menu: { - darkItemHoverBg: '#a51d1d', - itemHoverBg: '#a51d1d', - horizontalItemHoverBg: '#a51d1d', - } - }, - token: { - colorPrimary: '#a51d1d' + components: { + Menu: { + darkItemHoverBg: "#a51d1d", + itemHoverBg: "#a51d1d", + horizontalItemHoverBg: "#a51d1d" } + }, + token: { + colorPrimary: "#a51d1d" + } }; /** @@ -60,11 +60,10 @@ const devTheme = { */ const prodTheme = {}; -const currentTheme = import.meta.env.DEV ? devTheme - : prodTheme; +const currentTheme = import.meta.env.DEV ? devTheme : prodTheme; const finaltheme = { - algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, - ...defaultsDeep(currentTheme, defaultTheme) -} + algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, + ...defaultsDeep(currentTheme, defaultTheme) +}; export default finaltheme; diff --git a/client/src/assets/promanager/ios/Contents.json b/client/src/assets/promanager/ios/Contents.json index bd04914ae..0767e12cf 100644 --- a/client/src/assets/promanager/ios/Contents.json +++ b/client/src/assets/promanager/ios/Contents.json @@ -131,4 +131,4 @@ "author": "iconkitchen", "version": 1 } -} \ No newline at end of file +} diff --git a/client/src/components/PrivateRoute.jsx b/client/src/components/PrivateRoute.jsx index ebb7ece93..9286ae2d6 100644 --- a/client/src/components/PrivateRoute.jsx +++ b/client/src/components/PrivateRoute.jsx @@ -1,17 +1,17 @@ -import React, {useEffect} from "react"; -import {Outlet, useLocation, useNavigate} from "react-router-dom"; +import React, { useEffect } from "react"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; -function PrivateRoute({component: Component, isAuthorized, ...rest}) { - const location = useLocation(); - const navigate = useNavigate(); +function PrivateRoute({ component: Component, isAuthorized, ...rest }) { + const location = useLocation(); + const navigate = useNavigate(); - useEffect(() => { - if (!isAuthorized) { - navigate(`/signin?redirect=${location.pathname}`); - } - }, [isAuthorized, navigate, location]); + useEffect(() => { + if (!isAuthorized) { + navigate(`/signin?redirect=${location.pathname}`); + } + }, [isAuthorized, navigate, location]); - return ; + return ; } export default PrivateRoute; diff --git a/client/src/components/_test/test.page.jsx b/client/src/components/_test/test.page.jsx index df1d9b41b..10af3b10b 100644 --- a/client/src/components/_test/test.page.jsx +++ b/client/src/components/_test/test.page.jsx @@ -1,31 +1,30 @@ -import {Button} from "antd"; +import { Button } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setModalContext} from "../../redux/modals/modals.actions"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = (dispatch) => ({ - setRefundPaymentContext: (context) => - dispatch(setModalContext({context: context, modal: "refund_payment"})), + setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" })) }); -function Test({setRefundPaymentContext, refundPaymentModal}) { - console.log("refundPaymentModal", refundPaymentModal); - return ( - - - setRefundPaymentContext({ - context: {}, - }) - } - > - Open Modal - - - ); +function Test({ setRefundPaymentContext, refundPaymentModal }) { + console.log("refundPaymentModal", refundPaymentModal); + return ( + + + setRefundPaymentContext({ + context: {} + }) + } + > + Open Modal + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(Test); diff --git a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx index bb3201fcc..f501b3dbb 100644 --- a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx +++ b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx @@ -1,15 +1,16 @@ -import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect } from "react-redux"; -import {Link} from "react-router-dom"; +import { Card, Checkbox, Input, Space, Table } from "antd"; +import queryString from "query-string"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {DateFormatter} from "../../utils/DateFormatter"; +import { DateFormatter } from "../../utils/DateFormatter"; import { pageLimit } from "../../utils/config"; -import {alphaSort, dateSort} from "../../utils/sorters"; +import { alphaSort, dateSort } from "../../utils/sorters"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component"; import PayableExportButton from "../payable-export-button/payable-export-button.component"; @@ -17,216 +18,179 @@ import BillMarkSelectedExported from "../payable-mark-selected-exported/payable- import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(AccountingPayablesTableComponent); +export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent); -export function AccountingPayablesTableComponent({ - bodyshop, - loading, - bills, - refetch, - }) { - const {t} = useTranslation(); - const [selectedBills, setSelectedBills] = useState([]); - const [transInProgress, setTransInProgress] = useState(false); - const [state, setState] = useState({ - sortedInfo: {}, - search: "", - }); +export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) { + const { t } = useTranslation(); + const [selectedBills, setSelectedBills] = useState([]); + const [transInProgress, setTransInProgress] = useState(false); + const [state, setState] = useState({ + sortedInfo: {}, + search: "" + }); - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const columns = [ - { - title: t("bills.fields.vendorname"), - dataIndex: "vendorname", - key: "vendorname", - sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), - sortOrder: - state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, - render: (text, record) => ( - - {record.vendor.name} - - ), - }, - { - title: t("bills.fields.invoice_number"), - dataIndex: "invoice_number", - key: "invoice_number", - sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), - sortOrder: - state.sortedInfo.columnKey === "invoice_number" && - state.sortedInfo.order, - render: (text, record) => ( - - {record.invoice_number} - - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - {record.job.ro_number} - ), - }, - { - title: t("bills.fields.date"), - dataIndex: "date", - key: "date", - - sorter: (a, b) => dateSort(a.date, b.date), - sortOrder: - state.sortedInfo.columnKey === "date" && state.sortedInfo.order, - render: (text, record) => {record.date}, - }, - { - title: t("bills.fields.total"), - dataIndex: "total", - key: "total", - - sorter: (a, b) => a.total - b.total, - sortOrder: - state.sortedInfo.columnKey === "total" && state.sortedInfo.order, - render: (text, record) => ( - {record.total} - ), - }, - { - title: t("bills.fields.is_credit_memo"), - dataIndex: "is_credit_memo", - key: "is_credit_memo", - sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, - sortOrder: - state.sortedInfo.columnKey === "is_credit_memo" && - state.sortedInfo.order, - render: (text, record) => ( - - ), - }, - { - title: t("exportlogs.labels.attempts"), - dataIndex: "attempts", - key: "attempts", - - render: (text, record) => ( - - ), - }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", - - - render: (text, record) => ( - - ), - }, - ]; - - const handleSearch = (e) => { - setState({...state, search: e.target.value}); - logImEXEvent("accounting_payables_table_search"); - }; - - const dataSource = state.search - ? bills.filter( - (v) => - (v.vendor.name || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.invoice_number || "") - .toLowerCase() - .includes(state.search.toLowerCase()) - ) - : bills; - - return ( - - - - {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( - - )} - - - } + const columns = [ + { + title: t("bills.fields.vendorname"), + dataIndex: "vendorname", + key: "vendorname", + sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), + sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, + render: (text, record) => ( + - - setSelectedBills(selectedRows.map((i) => i.id)), - onSelect: (record, selected, selectedRows, nativeEvent) => { - setSelectedBills(selectedRows.map((i) => i.id)); - }, - getCheckboxProps: (record) => ({ - disabled: record.exported, - }), - selectedRowKeys: selectedBills, - type: "checkbox", - }} - /> - - ); + {record.vendor.name} + + ) + }, + { + title: t("bills.fields.invoice_number"), + dataIndex: "invoice_number", + key: "invoice_number", + sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), + sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order, + render: (text, record) => ( + + {record.invoice_number} + + ) + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => {record.job.ro_number} + }, + { + title: t("bills.fields.date"), + dataIndex: "date", + key: "date", + + sorter: (a, b) => dateSort(a.date, b.date), + sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order, + render: (text, record) => {record.date} + }, + { + title: t("bills.fields.total"), + dataIndex: "total", + key: "total", + + sorter: (a, b) => a.total - b.total, + sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order, + render: (text, record) => {record.total} + }, + { + title: t("bills.fields.is_credit_memo"), + dataIndex: "is_credit_memo", + key: "is_credit_memo", + sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, + sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order, + render: (text, record) => + }, + { + title: t("exportlogs.labels.attempts"), + dataIndex: "attempts", + key: "attempts", + + render: (text, record) => + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + + render: (text, record) => ( + + ) + } + ]; + + const handleSearch = (e) => { + setState({ ...state, search: e.target.value }); + logImEXEvent("accounting_payables_table_search"); + }; + + const dataSource = state.search + ? bills.filter( + (v) => + (v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase()) + ) + : bills; + + return ( + + + + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && } + + + } + > + setSelectedBills(selectedRows.map((i) => i.id)), + onSelect: (record, selected, selectedRows, nativeEvent) => { + setSelectedBills(selectedRows.map((i) => i.id)); + }, + getCheckboxProps: (record) => ({ + disabled: record.exported + }), + selectedRowKeys: selectedBills, + type: "checkbox" + }} + /> + + ); } diff --git a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx index 9f1c37651..f6040fab2 100644 --- a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx +++ b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx @@ -1,242 +1,198 @@ -import {Card, Input, Space, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {Link} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { Card, Input, Space, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter"; -import {pageLimit } from "../../utils/config"; -import {alphaSort, dateSort} from "../../utils/sorters"; +import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, dateSort } from "../../utils/sorters"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; -import OwnerNameDisplay, { - OwnerNameDisplayFunction, -} from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(AccountingPayablesTableComponent); +export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent); -export function AccountingPayablesTableComponent({ - bodyshop, - loading, - payments, - refetch, - }) { - const {t} = useTranslation(); - const [selectedPayments, setSelectedPayments] = useState([]); - const [transInProgress, setTransInProgress] = useState(false); - const [state, setState] = useState({ - sortedInfo: {}, - search: "", - }); +export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) { + const { t } = useTranslation(); + const [selectedPayments, setSelectedPayments] = useState([]); + const [transInProgress, setTransInProgress] = useState(false); + const [state, setState] = useState({ + sortedInfo: {}, + search: "" + }); - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - {record.job.ro_number} - ), - }, - { - title: t("payments.fields.date"), - dataIndex: "date", - key: "date", - sorter: (a, b) => dateSort(a.date, b.date), - sortOrder: - state.sortedInfo.columnKey === "date" && state.sortedInfo.order, - render: (text, record) => {record.date}, - }, + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => {record.job.ro_number} + }, + { + title: t("payments.fields.date"), + dataIndex: "date", + key: "date", + sorter: (a, b) => dateSort(a.date, b.date), + sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order, + render: (text, record) => {record.date} + }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => - alphaSort( - OwnerNameDisplayFunction(a.job), - OwnerNameDisplayFunction(b.job) - ), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - return record.job.owner ? ( - - - - ) : ( - - + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.job.owner ? ( + + + + ) : ( + + - ); - }, - }, - { - title: t("payments.fields.amount"), - dataIndex: "amount", - key: "amount", - sorter: (a, b) => a.amount - b.amount, - sortOrder: - state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => ( - {record.amount} - ), - }, - { - title: t("payments.fields.memo"), - dataIndex: "memo", - key: "memo", - }, - { - title: t("payments.fields.transactionid"), - dataIndex: "transactionid", - key: "transactionid", - }, - { - title: t("payments.fields.created_at"), - dataIndex: "created_at", - key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at), - sortOrder: - state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order, - render: (text, record) => ( - {record.created_at} - ), - }, - //{ - // title: t("payments.fields.exportedat"), - // dataIndex: "exportedat", - // key: "exportedat", - // render: (text, record) => ( - // {record.exportedat} - // ), - //}, - { - title: t("exportlogs.labels.attempts"), - dataIndex: "attempts", - key: "attempts", + ); + } + }, + { + title: t("payments.fields.amount"), + dataIndex: "amount", + key: "amount", + sorter: (a, b) => a.amount - b.amount, + sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order, + render: (text, record) => {record.amount} + }, + { + title: t("payments.fields.memo"), + dataIndex: "memo", + key: "memo" + }, + { + title: t("payments.fields.transactionid"), + dataIndex: "transactionid", + key: "transactionid" + }, + { + title: t("payments.fields.created_at"), + dataIndex: "created_at", + key: "created_at", + sorter: (a, b) => dateSort(a.created_at, b.created_at), + sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order, + render: (text, record) => {record.created_at} + }, + //{ + // title: t("payments.fields.exportedat"), + // dataIndex: "exportedat", + // key: "exportedat", + // render: (text, record) => ( + // {record.exportedat} + // ), + //}, + { + title: t("exportlogs.labels.attempts"), + dataIndex: "attempts", + key: "attempts", - render: (text, record) => ( - - ), - }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", + render: (text, record) => + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => ( + + ) + } + ]; - render: (text, record) => ( - - ), - }, - ]; + const handleSearch = (e) => { + setState({ ...state, search: e.target.value }); + logImEXEvent("account_payments_table_search"); + }; - const handleSearch = (e) => { - setState({...state, search: e.target.value}); - logImEXEvent("account_payments_table_search"); - }; + const dataSource = state.search + ? payments.filter( + (v) => + (v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) || + ((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) || + ((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) || + ((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) || + ((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.toLowerCase()) + ) + : payments; - const dataSource = state.search - ? payments.filter( - (v) => - (v.paymentnum || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - ((v.job && v.job.ro_number) || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - ((v.job && v.job.ownr_fn) || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - ((v.job && v.job.ownr_ln) || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - ((v.job && v.job.ownr_co_nm) || "") - .toLowerCase() - .includes(state.search.toLowerCase()) - ) - : payments; - - return ( - - - - {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( - - )} - - - } - > - - setSelectedPayments(selectedRows.map((i) => i.id)), - onSelect: (record, selected, selectedRows, nativeEvent) => { - setSelectedPayments(selectedRows.map((i) => i.id)); - }, - getCheckboxProps: (record) => ({ - disabled: record.exported, - }), - selectedRowKeys: selectedPayments, - type: "checkbox", - }} - /> - - ); + return ( + + + + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && } + + + } + > + setSelectedPayments(selectedRows.map((i) => i.id)), + onSelect: (record, selected, selectedRows, nativeEvent) => { + setSelectedPayments(selectedRows.map((i) => i.id)); + }, + getCheckboxProps: (record) => ({ + disabled: record.exported + }), + selectedRowKeys: selectedPayments, + type: "checkbox" + }} + /> + + ); } diff --git a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx index 1fe3cbf4d..83b139827 100644 --- a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx +++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx @@ -1,258 +1,212 @@ -import {Button, Card, Input, Space, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Link} from "react-router-dom"; -import {logImEXEvent} from "../../firebase/firebase.utils"; +import { Button, Card, Input, Space, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {alphaSort, dateSort, statusSort } from "../../utils/sorters"; +import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {DateFormatter} from "../../utils/DateFormatter"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { DateFormatter } from "../../utils/DateFormatter"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; -import OwnerNameDisplay, { - OwnerNameDisplayFunction, -} from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(AccountingReceivablesTableComponent); +export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent); -export function AccountingReceivablesTableComponent({ - bodyshop, - loading, - jobs, - refetch, - }) { - const {t} = useTranslation(); - const [selectedJobs, setSelectedJobs] = useState([]); - const [transInProgress, setTransInProgress] = useState(false); +export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) { + const { t } = useTranslation(); + const [selectedJobs, setSelectedJobs] = useState([]); + const [transInProgress, setTransInProgress] = useState(false); - const [state, setState] = useState({ - sortedInfo: {}, - search: "", - }); + const [state, setState] = useState({ + sortedInfo: {}, + search: "" + }); - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - {record.ro_number} - ), - }, + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => {record.ro_number} + }, - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - }, - { - title: t("jobs.fields.date_invoiced"), - dataIndex: "date_invoiced", - key: "date_invoiced", - sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced), - sortOrder: - state.sortedInfo.columnKey === "date_invoiced" && - state.sortedInfo.order, - render: (text, record) => ( - {record.date_invoiced} - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - sorter: (a, b) => - alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - return record.owner ? ( - - - - ) : ( - - + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order + }, + { + title: t("jobs.fields.date_invoiced"), + dataIndex: "date_invoiced", + key: "date_invoiced", + sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced), + sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order, + render: (text, record) => {record.date_invoiced} + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.owner ? ( + + + + ) : ( + + - ); - }, - }, - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - sorter: (a, b) => + ); + } + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => alphaSort( - `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ - a.v_model_desc || "" - }`, + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` ), - sortOrder: - state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => { - return record.vehicleid ? ( - - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: - state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, - }, - { - title: t("jobs.fields.clm_total"), - dataIndex: "clm_total", - key: "clm_total", - sorter: (a, b) => a.clm_total - b.clm_total, - sortOrder: - state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, - render: (text, record) => { - return {record.clm_total}; - }, - }, - { - title: t("exportlogs.labels.attempts"), - dataIndex: "attempts", - key: "attempts", - render: (text, record) => ( - - ), - }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", + sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ); + } + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + ellipsis: true, + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), + sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order + }, + { + title: t("jobs.fields.clm_total"), + dataIndex: "clm_total", + key: "clm_total", + sorter: (a, b) => a.clm_total - b.clm_total, + sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, + render: (text, record) => { + return {record.clm_total}; + } + }, + { + title: t("exportlogs.labels.attempts"), + dataIndex: "attempts", + key: "attempts", + render: (text, record) => + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", - render: (text, record) => ( - - - - {t("jobs.labels.viewallocations")} - - - ), - }, - ]; + render: (text, record) => ( + + + + {t("jobs.labels.viewallocations")} + + + ) + } + ]; - const handleSearch = (e) => { - setState({...state, search: e.target.value}); - logImEXEvent("accounting_receivables_search"); - }; + const handleSearch = (e) => { + setState({ ...state, search: e.target.value }); + logImEXEvent("accounting_receivables_search"); + }; - const dataSource = state.search - ? jobs.filter( - (v) => - (v.ro_number || "") - .toString() - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.ownr_fn || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.ownr_ln || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.ownr_co_nm || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.v_model_desc || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.v_make_desc || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) - ) - : jobs; + const dataSource = state.search + ? jobs.filter( + (v) => + (v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) || + (v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) + ) + : jobs; - return ( - - {!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && ( - - )} - {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( - - )} - - - } - > - - setSelectedJobs(selectedRows.map((i) => i.id)), - onSelect: (record, selected, selectedRows, nativeEvent) => { - setSelectedJobs(selectedRows.map((i) => i.id)); - }, - getCheckboxProps: (record) => ({ - disabled: record.exported, - }), - selectedRowKeys: selectedJobs, - type: "checkbox", - }} + return ( + + {!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && ( + - - ); + )} + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && } + + + } + > + setSelectedJobs(selectedRows.map((i) => i.id)), + onSelect: (record, selected, selectedRows, nativeEvent) => { + setSelectedJobs(selectedRows.map((i) => i.id)); + }, + getCheckboxProps: (record) => ({ + disabled: record.exported + }), + selectedRowKeys: selectedJobs, + type: "checkbox" + }} + /> + + ); } diff --git a/client/src/components/alert/alert.component.jsx b/client/src/components/alert/alert.component.jsx index 5af5fce31..f02edd614 100644 --- a/client/src/components/alert/alert.component.jsx +++ b/client/src/components/alert/alert.component.jsx @@ -1,6 +1,6 @@ -import {Alert} from "antd"; +import { Alert } from "antd"; import React from "react"; export default function AlertComponent(props) { - return ; + return ; } diff --git a/client/src/components/alert/alert.component.test.js b/client/src/components/alert/alert.component.test.js index 32114c305..b7c14b48a 100644 --- a/client/src/components/alert/alert.component.test.js +++ b/client/src/components/alert/alert.component.test.js @@ -1,19 +1,19 @@ -import {shallow} from "enzyme"; +import { shallow } from "enzyme"; import React from "react"; import Alert from "./alert.component"; describe("Alert component", () => { - let wrapper; - beforeEach(() => { - const mockProps = { - type: "error", - message: "Test error message.", - }; + let wrapper; + beforeEach(() => { + const mockProps = { + type: "error", + message: "Test error message." + }; - wrapper = shallow(); - }); + wrapper = shallow(); + }); - it("should render Alert component", () => { - expect(wrapper).toMatchSnapshot(); - }); + it("should render Alert component", () => { + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/client/src/components/allocations-assignment/allocations-assignment.component.jsx b/client/src/components/allocations-assignment/allocations-assignment.component.jsx index 1d1804341..929d9eb75 100644 --- a/client/src/components/allocations-assignment/allocations-assignment.component.jsx +++ b/client/src/components/allocations-assignment/allocations-assignment.component.jsx @@ -1,72 +1,67 @@ -import {Button, InputNumber, Popover, Select} from "antd"; +import { Button, InputNumber, Popover, Select } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop + bodyshop: selectBodyshop }); export function AllocationsAssignmentComponent({ - bodyshop, - handleAssignment, - assignment, - setAssignment, - visibilityState, - maxHours - }) { - const {t} = useTranslation(); + bodyshop, + handleAssignment, + assignment, + setAssignment, + visibilityState, + maxHours +}) { + const { t } = useTranslation(); - const onChange = e => { - setAssignment({...assignment, employeeid: e}); - }; + const onChange = (e) => { + setAssignment({ ...assignment, employeeid: e }); + }; - const [visibility, setVisibility] = visibilityState; + const [visibility, setVisibility] = visibilityState; - const popContent = ( - - - option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 - }> - {bodyshop.employees.map(emp => ( - - {`${emp.first_name} ${emp.last_name}`} - - ))} - - setAssignment({...assignment, hours: e})} - /> + const popContent = ( + + option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0} + > + {bodyshop.employees.map((emp) => ( + + {`${emp.first_name} ${emp.last_name}`} + + ))} + + setAssignment({ ...assignment, hours: e })} + /> - - Assign - - setVisibility(false)}>Close - - ); + + Assign + + setVisibility(false)}>Close + + ); - return ( - - setVisibility(true)}> - {t("allocations.actions.assign")} - - - ); + return ( + + setVisibility(true)}>{t("allocations.actions.assign")} + + ); } export default connect(mapStateToProps, null)(AllocationsAssignmentComponent); diff --git a/client/src/components/allocations-assignment/allocations-assignment.component.test.js b/client/src/components/allocations-assignment/allocations-assignment.component.test.js index 8aad7efe6..fe9c3628b 100644 --- a/client/src/components/allocations-assignment/allocations-assignment.component.test.js +++ b/client/src/components/allocations-assignment/allocations-assignment.component.test.js @@ -1,35 +1,35 @@ -import {mount} from "enzyme"; +import { mount } from "enzyme"; import React from "react"; -import {MockBodyshop} from "../../utils/TestingHelpers"; -import {AllocationsAssignmentComponent} from "./allocations-assignment.component"; +import { MockBodyshop } from "../../utils/TestingHelpers"; +import { AllocationsAssignmentComponent } from "./allocations-assignment.component"; describe("AllocationsAssignmentComponent component", () => { - let wrapper; + let wrapper; - beforeEach(() => { - const mockProps = { - bodyshop: MockBodyshop, - handleAssignment: jest.fn(), - assignment: {}, - setAssignment: jest.fn(), - visibilityState: [false, jest.fn()], - maxHours: 4, - }; + beforeEach(() => { + const mockProps = { + bodyshop: MockBodyshop, + handleAssignment: jest.fn(), + assignment: {}, + setAssignment: jest.fn(), + visibilityState: [false, jest.fn()], + maxHours: 4 + }; - wrapper = mount(); - }); + wrapper = mount(); + }); - it("should render AllocationsAssignmentComponent component", () => { - expect(wrapper).toMatchSnapshot(); - }); + it("should render AllocationsAssignmentComponent component", () => { + expect(wrapper).toMatchSnapshot(); + }); - it("should render a list of employees", () => { - const empList = wrapper.find("#employeeSelector"); - expect(empList.children()).to.have.lengthOf(2); - }); + it("should render a list of employees", () => { + const empList = wrapper.find("#employeeSelector"); + expect(empList.children()).to.have.lengthOf(2); + }); - it("should create an allocation on save", () => { - wrapper.find("Button").simulate("click"); - expect(wrapper).toMatchSnapshot(); - }); + it("should create an allocation on save", () => { + wrapper.find("Button").simulate("click"); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/client/src/components/allocations-assignment/allocations-assignment.container.jsx b/client/src/components/allocations-assignment/allocations-assignment.container.jsx index 5153066cb..43d4b306c 100644 --- a/client/src/components/allocations-assignment/allocations-assignment.container.jsx +++ b/client/src/components/allocations-assignment/allocations-assignment.container.jsx @@ -1,47 +1,43 @@ -import React, {useState} from "react"; +import React, { useState } from "react"; import AllocationsAssignmentComponent from "./allocations-assignment.component"; -import {useMutation} from "@apollo/client"; -import {INSERT_ALLOCATION} from "../../graphql/allocations.queries"; -import {useTranslation} from "react-i18next"; -import {notification} from "antd"; +import { useMutation } from "@apollo/client"; +import { INSERT_ALLOCATION } from "../../graphql/allocations.queries"; +import { useTranslation } from "react-i18next"; +import { notification } from "antd"; -export default function AllocationsAssignmentContainer({ - jobLineId, - hours, - refetch, - }) { - const visibilityState = useState(false); - const {t} = useTranslation(); - const [assignment, setAssignment] = useState({ - joblineid: jobLineId, - hours: parseFloat(hours), - employeeid: null, - }); - const [insertAllocation] = useMutation(INSERT_ALLOCATION); +export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) { + const visibilityState = useState(false); + const { t } = useTranslation(); + const [assignment, setAssignment] = useState({ + joblineid: jobLineId, + hours: parseFloat(hours), + employeeid: null + }); + const [insertAllocation] = useMutation(INSERT_ALLOCATION); - const handleAssignment = () => { - insertAllocation({variables: {alloc: {...assignment}}}) - .then((r) => { - notification["success"]({ - message: t("allocations.successes.save"), - }); - visibilityState[1](false); - if (refetch) refetch(); - }) - .catch((error) => { - notification["error"]({ - message: t("employees.errors.saving", {message: error.message}), - }); - }); - }; + const handleAssignment = () => { + insertAllocation({ variables: { alloc: { ...assignment } } }) + .then((r) => { + notification["success"]({ + message: t("allocations.successes.save") + }); + visibilityState[1](false); + if (refetch) refetch(); + }) + .catch((error) => { + notification["error"]({ + message: t("employees.errors.saving", { message: error.message }) + }); + }); + }; - return ( - - ); + return ( + + ); } diff --git a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx index 2af0d75d0..301887fb7 100644 --- a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx +++ b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx @@ -1,68 +1,62 @@ -import {Button, Popover, Select} from "antd"; +import { Button, Popover, Select } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); export default connect( - mapStateToProps, - null + mapStateToProps, + null )(function AllocationsBulkAssignmentComponent({ - disabled, - bodyshop, - handleAssignment, - assignment, - setAssignment, - visibilityState, - }) { - const {t} = useTranslation(); + disabled, + bodyshop, + handleAssignment, + assignment, + setAssignment, + visibilityState +}) { + const { t } = useTranslation(); - const onChange = (e) => { - setAssignment({...assignment, employeeid: e}); - }; + const onChange = (e) => { + setAssignment({ ...assignment, employeeid: e }); + }; - const [visibility, setVisibility] = visibilityState; + const [visibility, setVisibility] = visibilityState; - const popContent = ( - - - option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 - } - > - {bodyshop.employees.map((emp) => ( - - {`${emp.first_name} ${emp.last_name}`} - - ))} - + const popContent = ( + + option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0} + > + {bodyshop.employees.map((emp) => ( + + {`${emp.first_name} ${emp.last_name}`} + + ))} + - - Assign - - setVisibility(false)}>Close - - ); + + Assign + + setVisibility(false)}>Close + + ); - return ( - - setVisibility(true)}> - {t("allocations.actions.assign")} - - - ); + return ( + + setVisibility(true)}> + {t("allocations.actions.assign")} + + + ); }); diff --git a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx index aad3ea8d2..ea68dd129 100644 --- a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx +++ b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx @@ -1,47 +1,44 @@ -import React, {useState} from "react"; +import React, { useState } from "react"; import AllocationsBulkAssignment from "./allocations-bulk-assignment.component"; -import {useMutation} from "@apollo/client"; -import {INSERT_ALLOCATION} from "../../graphql/allocations.queries"; -import {useTranslation} from "react-i18next"; -import {notification} from "antd"; +import { useMutation } from "@apollo/client"; +import { INSERT_ALLOCATION } from "../../graphql/allocations.queries"; +import { useTranslation } from "react-i18next"; +import { notification } from "antd"; -export default function AllocationsBulkAssignmentContainer({ - jobLines, - refetch, - }) { - const visibilityState = useState(false); - const {t} = useTranslation(); - const [assignment, setAssignment] = useState({ - employeeid: null, +export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) { + const visibilityState = useState(false); + const { t } = useTranslation(); + const [assignment, setAssignment] = useState({ + employeeid: null + }); + const [insertAllocation] = useMutation(INSERT_ALLOCATION); + + const handleAssignment = () => { + const allocs = jobLines.reduce((acc, value) => { + acc.push({ + joblineid: value.id, + hours: parseFloat(value.mod_lb_hrs) || 0, + employeeid: assignment.employeeid + }); + return acc; + }, []); + + insertAllocation({ variables: { alloc: allocs } }).then((r) => { + notification["success"]({ + message: t("employees.successes.save") + }); + visibilityState[1](false); + if (refetch) refetch(); }); - const [insertAllocation] = useMutation(INSERT_ALLOCATION); + }; - const handleAssignment = () => { - const allocs = jobLines.reduce((acc, value) => { - acc.push({ - joblineid: value.id, - hours: parseFloat(value.mod_lb_hrs) || 0, - employeeid: assignment.employeeid, - }); - return acc; - }, []); - - insertAllocation({variables: {alloc: allocs}}).then((r) => { - notification["success"]({ - message: t("employees.successes.save"), - }); - visibilityState[1](false); - if (refetch) refetch(); - }); - }; - - return ( - 0 ? false : true} - handleAssignment={handleAssignment} - assignment={assignment} - setAssignment={setAssignment} - visibilityState={visibilityState} - /> - ); + return ( + 0 ? false : true} + handleAssignment={handleAssignment} + assignment={assignment} + setAssignment={setAssignment} + visibilityState={visibilityState} + /> + ); } diff --git a/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx b/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx index 9f1447348..0adc01964 100644 --- a/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx +++ b/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx @@ -1,20 +1,14 @@ import Icon from "@ant-design/icons"; import React from "react"; -import {MdRemoveCircleOutline} from "react-icons/md"; +import { MdRemoveCircleOutline } from "react-icons/md"; -export default function AllocationsLabelComponent({allocation, handleClick}) { - return ( - +export default function AllocationsLabelComponent({ allocation, handleClick }) { + return ( + - {`${allocation.employee.first_name || ""} ${ - allocation.employee.last_name || "" - } (${allocation.hours || ""})`} + {`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`} - - - ); + + + ); } diff --git a/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx b/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx index e573e806b..44fe5d43e 100644 --- a/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx +++ b/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx @@ -1,32 +1,27 @@ import React from "react"; -import {useMutation} from "@apollo/client"; -import {DELETE_ALLOCATION} from "../../graphql/allocations.queries"; +import { useMutation } from "@apollo/client"; +import { DELETE_ALLOCATION } from "../../graphql/allocations.queries"; import AllocationsLabelComponent from "./allocations-employee-label.component"; -import {notification} from "antd"; -import {useTranslation} from "react-i18next"; +import { notification } from "antd"; +import { useTranslation } from "react-i18next"; -export default function AllocationsLabelContainer({allocation, refetch}) { - const [deleteAllocation] = useMutation(DELETE_ALLOCATION); - const {t} = useTranslation(); +export default function AllocationsLabelContainer({ allocation, refetch }) { + const [deleteAllocation] = useMutation(DELETE_ALLOCATION); + const { t } = useTranslation(); - const handleClick = (e) => { - e.preventDefault(); - deleteAllocation({variables: {id: allocation.id}}) - .then((r) => { - notification["success"]({ - message: t("allocations.successes.deleted"), - }); - if (refetch) refetch(); - }) - .catch((error) => { - notification["error"]({message: t("allocations.errors.deleting")}); - }); - }; + const handleClick = (e) => { + e.preventDefault(); + deleteAllocation({ variables: { id: allocation.id } }) + .then((r) => { + notification["success"]({ + message: t("allocations.successes.deleted") + }); + if (refetch) refetch(); + }) + .catch((error) => { + notification["error"]({ message: t("allocations.errors.deleting") }); + }); + }; - return ( - - ); + return ; } diff --git a/client/src/components/audit-trail-list/audit-trail-list.component.jsx b/client/src/components/audit-trail-list/audit-trail-list.component.jsx index 3f6decd80..9722005d6 100644 --- a/client/src/components/audit-trail-list/audit-trail-list.component.jsx +++ b/client/src/components/audit-trail-list/audit-trail-list.component.jsx @@ -1,85 +1,75 @@ -import React, {useState} from "react"; -import {Table} from "antd"; -import {alphaSort} from "../../utils/sorters"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; -import {useTranslation} from "react-i18next"; +import React, { useState } from "react"; +import { Table } from "antd"; +import { alphaSort } from "../../utils/sorters"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { useTranslation } from "react-i18next"; import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; -export default function AuditTrailListComponent({loading, data}) { - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {}, - }); - const {t} = useTranslation(); - const columns = [ - { - title: t("audit.fields.created"), - dataIndex: " created", - key: " created", - width: "10%", - render: (text, record) => ( - {record.created} - ), - sorter: (a, b) => a.created - b.created, - sortOrder: - state.sortedInfo.columnKey === "created" && state.sortedInfo.order, - }, - { - title: t("audit.fields.operation"), - dataIndex: "operation", - key: "operation", - width: "10%", - sorter: (a, b) => alphaSort(a.operation, b.operation), - sortOrder: - state.sortedInfo.columnKey === "operation" && state.sortedInfo.order, - }, - { - title: t("audit.fields.values"), - dataIndex: " old_val", - key: " old_val", - width: "10%", - render: (text, record) => ( - - ), - }, - { - title: t("audit.fields.useremail"), - dataIndex: "useremail", - key: "useremail", - width: "10%", - sorter: (a, b) => alphaSort(a.useremail, b.useremail), - sortOrder: - state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order, - }, - ]; +export default function AuditTrailListComponent({ loading, data }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: {} + }); + const { t } = useTranslation(); + const columns = [ + { + title: t("audit.fields.created"), + dataIndex: " created", + key: " created", + width: "10%", + render: (text, record) => {record.created}, + sorter: (a, b) => a.created - b.created, + sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order + }, + { + title: t("audit.fields.operation"), + dataIndex: "operation", + key: "operation", + width: "10%", + sorter: (a, b) => alphaSort(a.operation, b.operation), + sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order + }, + { + title: t("audit.fields.values"), + dataIndex: " old_val", + key: " old_val", + width: "10%", + render: (text, record) => + }, + { + title: t("audit.fields.useremail"), + dataIndex: "useremail", + key: "useremail", + width: "10%", + sorter: (a, b) => alphaSort(a.useremail, b.useremail), + sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order + } + ]; - const formItemLayout = { - labelCol: { - xs: {span: 12}, - sm: {span: 5}, - }, - wrapperCol: { - xs: {span: 24}, - sm: {span: 12}, - }, - }; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const formItemLayout = { + labelCol: { + xs: { span: 12 }, + sm: { span: 5 } + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 } + } + }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - return ( - - ); + return ( + + ); } diff --git a/client/src/components/audit-trail-list/audit-trail-list.container.jsx b/client/src/components/audit-trail-list/audit-trail-list.container.jsx index cd5672943..6361bc240 100644 --- a/client/src/components/audit-trail-list/audit-trail-list.container.jsx +++ b/client/src/components/audit-trail-list/audit-trail-list.container.jsx @@ -1,40 +1,34 @@ import React from "react"; import AuditTrailListComponent from "./audit-trail-list.component"; -import {useQuery} from "@apollo/client"; -import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries"; +import { useQuery } from "@apollo/client"; +import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries"; import AlertComponent from "../alert/alert.component"; -import {logImEXEvent} from "../../firebase/firebase.utils"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import EmailAuditTrailListComponent from "./email-audit-trail-list.component"; -import {Card, Row} from "antd"; +import { Card, Row } from "antd"; -export default function AuditTrailListContainer({recordId}) { - const {loading, error, data} = useQuery(QUERY_AUDIT_TRAIL, { - variables: {id: recordId}, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +export default function AuditTrailListContainer({ recordId }) { + const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, { + variables: { id: recordId }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - logImEXEvent("audittrail_view", {recordId}); - return ( - - {error ? ( - - ) : ( - - - - - - - - - )} - - ); + logImEXEvent("audittrail_view", { recordId }); + return ( + + {error ? ( + + ) : ( + + + + + + + + + )} + + ); } diff --git a/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx b/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx index 015b0d962..5f462d500 100644 --- a/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx +++ b/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx @@ -1,64 +1,60 @@ -import {Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; -import {alphaSort} from "../../utils/sorters"; -import {pageLimit} from "../../utils/config"; +import { Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { alphaSort } from "../../utils/sorters"; +import { pageLimit } from "../../utils/config"; -export default function EmailAuditTrailListComponent({loading, data}) { - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {}, - }); - const {t} = useTranslation(); - const columns = [ - { - title: t("audit.fields.created"), - dataIndex: " created", - key: " created", - width: "10%", - render: (text, record) => ( - {record.created} - ), - sorter: (a, b) => a.created - b.created, - sortOrder: - state.sortedInfo.columnKey === "created" && state.sortedInfo.order, - }, +export default function EmailAuditTrailListComponent({ loading, data }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: {} + }); + const { t } = useTranslation(); + const columns = [ + { + title: t("audit.fields.created"), + dataIndex: " created", + key: " created", + width: "10%", + render: (text, record) => {record.created}, + sorter: (a, b) => a.created - b.created, + sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order + }, - { - title: t("audit.fields.useremail"), - dataIndex: "useremail", - key: "useremail", - width: "10%", - sorter: (a, b) => alphaSort(a.useremail, b.useremail), - sortOrder: - state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order, - }, - ]; + { + title: t("audit.fields.useremail"), + dataIndex: "useremail", + key: "useremail", + width: "10%", + sorter: (a, b) => alphaSort(a.useremail, b.useremail), + sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order + } + ]; - const formItemLayout = { - labelCol: { - xs: {span: 12}, - sm: {span: 5}, - }, - wrapperCol: { - xs: {span: 24}, - sm: {span: 12}, - }, - }; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const formItemLayout = { + labelCol: { + xs: { span: 12 }, + sm: { span: 5 } + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 } + } + }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - return ( - - ); + return ( + + ); } diff --git a/client/src/components/audit-trail-values/audit-trail-values.component.jsx b/client/src/components/audit-trail-values/audit-trail-values.component.jsx index 3838a60dd..b3c531927 100644 --- a/client/src/components/audit-trail-values/audit-trail-values.component.jsx +++ b/client/src/components/audit-trail-values/audit-trail-values.component.jsx @@ -1,30 +1,30 @@ import React from "react"; -import {List} from "antd"; +import { List } from "antd"; import Icon from "@ant-design/icons"; -import {FaArrowRight} from "react-icons/fa"; +import { FaArrowRight } from "react-icons/fa"; -export default function AuditTrailValuesComponent({oldV, newV}) { - if (!oldV && !newV) return ; - - if (!oldV && newV) - return ( - - {Object.keys(newV).map((key, idx) => ( - - {key}: {JSON.stringify(newV[key])} - - ))} - - ); +export default function AuditTrailValuesComponent({ oldV, newV }) { + if (!oldV && !newV) return ; + if (!oldV && newV) return ( - - {Object.keys(oldV).map((key, idx) => ( - - {key}: {oldV[key]} - {JSON.stringify(newV[key])} - - ))} - + + {Object.keys(newV).map((key, idx) => ( + + {key}: {JSON.stringify(newV[key])} + + ))} + ); + + return ( + + {Object.keys(oldV).map((key, idx) => ( + + {key}: {oldV[key]} + {JSON.stringify(newV[key])} + + ))} + + ); } diff --git a/client/src/components/barcode-popup/barcode-popup.component.jsx b/client/src/components/barcode-popup/barcode-popup.component.jsx index 457de3b49..c6dbb4ee3 100644 --- a/client/src/components/barcode-popup/barcode-popup.component.jsx +++ b/client/src/components/barcode-popup/barcode-popup.component.jsx @@ -1,23 +1,15 @@ -import {Popover, Tag} from "antd"; +import { Popover, Tag } from "antd"; import React from "react"; import Barcode from "react-barcode"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; -export default function BarcodePopupComponent({value, children}) { - const {t} = useTranslation(); - return ( - - - } - > - {children ? children : {t("general.labels.barcode")}} - - - ); +export default function BarcodePopupComponent({ value, children }) { + const { t } = useTranslation(); + return ( + + }> + {children ? children : {t("general.labels.barcode")}} + + + ); } diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx index 7e2e43dd2..5eb8bb4fc 100644 --- a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx +++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx @@ -1,136 +1,128 @@ -import {Checkbox, Form, Skeleton, Typography} from "antd"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; +import { Checkbox, Form, Skeleton, Typography } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import "./bill-cm-returns-table.styles.scss"; -export default function BillCmdReturnsTableComponent({ - form, - returnLoading, - returnData, - }) { - const {t} = useTranslation(); +export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) { + const { t } = useTranslation(); - useEffect(() => { - if (returnData) { - form.setFieldsValue({ - outstanding_returns: returnData.parts_order_lines, - }); + useEffect(() => { + if (returnData) { + form.setFieldsValue({ + outstanding_returns: returnData.parts_order_lines + }); + } + }, [returnData, form]); + + return ( + + prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid + } + noStyle + > + {() => { + const isReturn = form.getFieldValue("is_credit_memo"); + + if (!isReturn) { + return null; } - }, [returnData, form]); - return ( - - prev.jobid !== cur.jobid || - prev.is_credit_memo !== cur.is_credit_memo || - prev.vendorid !== cur.vendorid - } - noStyle - > - {() => { - const isReturn = form.getFieldValue("is_credit_memo"); + if (returnLoading) return ; - if (!isReturn) { - return null; - } + return ( + + {(fields, { add, remove, move }) => { + return ( + <> + {t("bills.labels.creditsnotreceived")} + + + + {t("parts_orders.fields.line_desc")} + {t("parts_orders.fields.part_type")} + {t("parts_orders.fields.quantity")} + {t("parts_orders.fields.act_price")} + {t("parts_orders.fields.cost")} + {t("parts_orders.labels.mark_as_received")} + + + + {fields.map((field, index) => ( + + + + + + - if (returnLoading) return ; + + + + + + + + + + + + + + + + + + + + - return ( - - {(fields, {add, remove, move}) => { - return ( - <> - - {t("bills.labels.creditsnotreceived")} - - - - - {t("parts_orders.fields.line_desc")} - {t("parts_orders.fields.part_type")} - {t("parts_orders.fields.quantity")} - {t("parts_orders.fields.act_price")} - {t("parts_orders.fields.cost")} - {t("parts_orders.labels.mark_as_received")} - - - - {fields.map((field, index) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - - - > - ); - }} - - ); + + + + + + + ))} + + + > + ); }} - - ); + + ); + }} + + ); } diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss index 2b1a11fc7..743ef7ac0 100644 --- a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss +++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss @@ -16,4 +16,4 @@ tr:hover { background-color: #f5f5f5; } -} \ No newline at end of file +} diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx index 13eb94527..55c9a9072 100644 --- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx +++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx @@ -1,97 +1,88 @@ -import {DeleteFilled} from "@ant-design/icons"; -import {useMutation} from "@apollo/client"; -import {Button, notification, Popconfirm} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {DELETE_BILL} from "../../graphql/bills.queries"; +import { DeleteFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, notification, Popconfirm } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DELETE_BILL } from "../../graphql/bills.queries"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; -import {insertAuditTrail} from "../../redux/application/application.actions"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = (dispatch) => ({ - insertAuditTrail: ({ jobid, operation, type }) => - dispatch(insertAuditTrail({ jobid, operation, type })), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton); export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) { - const [loading, setLoading] = useState(false); - const { t } = useTranslation(); - const [deleteBill] = useMutation(DELETE_BILL); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + const [deleteBill] = useMutation(DELETE_BILL); - const handleDelete = async () => { - setLoading(true); - const result = await deleteBill({ - variables: {billId: bill.id}, - update(cache, {errors}) { - if (errors) return; - cache.modify({ - fields: { - bills(existingBills, {readField}) { - return existingBills.filter( - (billref) => bill.id !== readField("id", billref) - ); - }, - search_bills(existingBills, {readField}) { - return existingBills.filter( - (billref) => bill.id !== readField("id", billref) - ); - }, - }, - }); + const handleDelete = async () => { + setLoading(true); + const result = await deleteBill({ + variables: { billId: bill.id }, + update(cache, { errors }) { + if (errors) return; + cache.modify({ + fields: { + bills(existingBills, { readField }) { + return existingBills.filter((billref) => bill.id !== readField("id", billref)); }, + search_bills(existingBills, { readField }) { + return existingBills.filter((billref) => bill.id !== readField("id", billref)); + } + } }); + } + }); - if (!!!result.errors) { - notification["success"]({ message: t("bills.successes.deleted") }); - insertAuditTrail({ - jobid: jobid, - operation: AuditTrailMapping.billdeleted(bill.invoice_number), - type: "billdeleted", + if (!!!result.errors) { + notification["success"]({ message: t("bills.successes.deleted") }); + insertAuditTrail({ + jobid: jobid, + operation: AuditTrailMapping.billdeleted(bill.invoice_number), + type: "billdeleted" }); - if (callback && typeof callback === "function") callback(bill.id); - } else { - //Check if it's an fkey violation. - const error = JSON.stringify(result.errors); + if (callback && typeof callback === "function") callback(bill.id); + } else { + //Check if it's an fkey violation. + const error = JSON.stringify(result.errors); - if (error.toLowerCase().includes("inventory_billid_fkey")) { - notification["error"]({ - message: t("bills.errors.deleting", { - error: t("bills.errors.existinginventoryline"), - }), - }); - } else { - notification["error"]({ - message: t("bills.errors.deleting", { - error: JSON.stringify(result.errors), - }), - }); - } - } + if (error.toLowerCase().includes("inventory_billid_fkey")) { + notification["error"]({ + message: t("bills.errors.deleting", { + error: t("bills.errors.existinginventoryline") + }) + }); + } else { + notification["error"]({ + message: t("bills.errors.deleting", { + error: JSON.stringify(result.errors) + }) + }); + } + } - setLoading(false); - }; + setLoading(false); + }; - return ( - >}> - - - - - - - ); + return ( + >}> + + + + + + + ); } diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index bc763be77..f5ce3d3d4 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -1,17 +1,17 @@ -import {useMutation, useQuery} from "@apollo/client"; -import {Button, Divider, Form, Popconfirm, Space} from "antd"; +import { useMutation, useQuery } from "@apollo/client"; +import { Button, Divider, Form, Popconfirm, Space } from "antd"; import dayjs from "../../utils/day"; import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useLocation} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE} from "../../graphql/bill-lines.queries"; -import {QUERY_BILL_BY_PK, UPDATE_BILL} from "../../graphql/bills.queries"; -import {insertAuditTrail} from "../../redux/application/application.actions"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE } from "../../graphql/bill-lines.queries"; +import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AlertComponent from "../alert/alert.component"; import BillFormContainer from "../bill-form/bill-form.container"; @@ -22,227 +22,212 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import BillDetailEditReturn from "./bill-detail-edit-return.component"; -import {PageHeader} from "@ant-design/pro-layout"; +import { PageHeader } from "@ant-design/pro-layout"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setPartsOrderContext: (context) => - dispatch(setModalContext({context: context, modal: "partsOrder"})), - insertAuditTrail: ({jobid, operation, type}) => - dispatch(insertAuditTrail({jobid, operation, type })), + setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillDetailEditcontainer); +export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer); -export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) { - const search = queryString.parse(useLocation().search); +export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) { + const search = queryString.parse(useLocation().search); - const {t} = useTranslation(); - const [form] = Form.useForm(); - const [open, setOpen] = useState(false); - const [updateLoading, setUpdateLoading] = useState(false); - const [update_bill] = useMutation(UPDATE_BILL); - const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); - const [updateBillLine] = useMutation(UPDATE_BILL_LINE); - const [deleteBillLine] = useMutation(DELETE_BILL_LINE); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [open, setOpen] = useState(false); + const [updateLoading, setUpdateLoading] = useState(false); + const [update_bill] = useMutation(UPDATE_BILL); + const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); + const [updateBillLine] = useMutation(UPDATE_BILL_LINE); + const [deleteBillLine] = useMutation(DELETE_BILL_LINE); - const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, { - variables: {billid: search.billid}, - skip: !!!search.billid, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, { + variables: { billid: search.billid }, + skip: !!!search.billid, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - // ... rest of the code remains the same + // ... rest of the code remains the same - const handleSave = () => { - //It's got a previously deducted bill line! - if ( - data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || - form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > - 0 - ) - setOpen(true); - else { - form.submit(); - } - }; + const handleSave = () => { + //It's got a previously deducted bill line! + if ( + data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || + form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0 + ) + setOpen(true); + else { + form.submit(); + } + }; - const handleFinish = async (values) => { - setUpdateLoading(true); - //let adjustmentsToInsert = {}; + const handleFinish = async (values) => { + setUpdateLoading(true); + //let adjustmentsToInsert = {}; - const {billlines, upload, ...bill} = values; - const updates = []; - updates.push( - update_bill({ - variables: {billId: search.billid, bill: bill}, - }) - ); - - billlines.forEach((l) => { - delete l.selected; - }); - - //Find bill lines that were deleted. - const deletedJobLines = []; - - data.bills_by_pk.billlines.forEach((a) => { - const matchingRecord = billlines.find((b) => b.id === a.id); - if (!matchingRecord) { - deletedJobLines.push(a); - } - }); - - deletedJobLines.forEach((d) => { - updates.push(deleteBillLine({variables: {id: d.id}})); - }); - - billlines.forEach((billline) => { - const {deductedfromlbr, inventories, jobline, ...il} = billline; - delete il.__typename; - - if (il.id) { - updates.push( - updateBillLine({ - variables: { - billLineId: il.id, - billLine: { - ...il, - deductedfromlbr: deductedfromlbr, - joblineid: il.joblineid === "noline" ? null : il.joblineid, - }, - }, - }) - ); - } else { - //It's a new line, have to insert it. - updates.push( - insertBillLine({ - variables: { - billLines: [ - { - ...il, - deductedfromlbr: deductedfromlbr, - billid: search.billid, - joblineid: il.joblineid === "noline" ? null : il.joblineid, - }, - ], - }, - }) - ); - } - }); - - await Promise.all(updates); - - insertAuditTrail({ - jobid: bill.jobid, - billid: search.billid, - operation: AuditTrailMapping.billupdated(bill.invoice_number), - type: "billupdated", - }); - - await refetch(); - form.setFieldsValue(transformData(data)); - form.resetFields(); - setOpen(false); - setUpdateLoading(false); - }; - - if (error) return ; - if (!search.billid) return <>>; //{t("bills.labels.noneselected")}; - - const exported = data && data.bills_by_pk && data.bills_by_pk.exported; - - return ( - <> - {loading && } - {data && ( - <> - - - - form.submit()} - onCancel={() => setOpen(false)} - okButtonProps={{loading: updateLoading}} - title={t("bills.labels.editadjwarning")} - > - - {t("general.actions.save")} - - - - - - } - /> - - - {t("general.labels.media")} - {bodyshop.uselocalmediaserver ? ( - - ) : ( - - )} - - > - )} - > + const { billlines, upload, ...bill } = values; + const updates = []; + updates.push( + update_bill({ + variables: { billId: search.billid, bill: bill } + }) ); + + billlines.forEach((l) => { + delete l.selected; + }); + + //Find bill lines that were deleted. + const deletedJobLines = []; + + data.bills_by_pk.billlines.forEach((a) => { + const matchingRecord = billlines.find((b) => b.id === a.id); + if (!matchingRecord) { + deletedJobLines.push(a); + } + }); + + deletedJobLines.forEach((d) => { + updates.push(deleteBillLine({ variables: { id: d.id } })); + }); + + billlines.forEach((billline) => { + const { deductedfromlbr, inventories, jobline, ...il } = billline; + delete il.__typename; + + if (il.id) { + updates.push( + updateBillLine({ + variables: { + billLineId: il.id, + billLine: { + ...il, + deductedfromlbr: deductedfromlbr, + joblineid: il.joblineid === "noline" ? null : il.joblineid + } + } + }) + ); + } else { + //It's a new line, have to insert it. + updates.push( + insertBillLine({ + variables: { + billLines: [ + { + ...il, + deductedfromlbr: deductedfromlbr, + billid: search.billid, + joblineid: il.joblineid === "noline" ? null : il.joblineid + } + ] + } + }) + ); + } + }); + + await Promise.all(updates); + + insertAuditTrail({ + jobid: bill.jobid, + billid: search.billid, + operation: AuditTrailMapping.billupdated(bill.invoice_number), + type: "billupdated" + }); + + await refetch(); + form.setFieldsValue(transformData(data)); + form.resetFields(); + setOpen(false); + setUpdateLoading(false); + }; + + if (error) return ; + if (!search.billid) return <>>; //{t("bills.labels.noneselected")}; + + const exported = data && data.bills_by_pk && data.bills_by_pk.exported; + + return ( + <> + {loading && } + {data && ( + <> + + + + form.submit()} + onCancel={() => setOpen(false)} + okButtonProps={{ loading: updateLoading }} + title={t("bills.labels.editadjwarning")} + > + + {t("general.actions.save")} + + + + + + } + /> + + + {t("general.labels.media")} + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} + + > + )} + > + ); } const transformData = (data) => { - return data - ? { - ...data.bills_by_pk, + return data + ? { + ...data.bills_by_pk, - billlines: data.bills_by_pk.billlines.map((i) => { - return { - ...i, - joblineid: !!i.joblineid ? i.joblineid : "noline", - applicable_taxes: { - federal: - (i.applicable_taxes && i.applicable_taxes.federal) || false, - state: (i.applicable_taxes && i.applicable_taxes.state) || false, - local: (i.applicable_taxes && i.applicable_taxes.local) || false, - }, - }; - }), - date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null, - } - : {}; + billlines: data.bills_by_pk.billlines.map((i) => { + return { + ...i, + joblineid: !!i.joblineid ? i.joblineid : "noline", + applicable_taxes: { + federal: (i.applicable_taxes && i.applicable_taxes.federal) || false, + state: (i.applicable_taxes && i.applicable_taxes.state) || false, + local: (i.applicable_taxes && i.applicable_taxes.local) || false + } + }; + }), + date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null + } + : {}; }; diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx index 62ac07258..44833c1b5 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx @@ -1,189 +1,168 @@ -import {Button, Checkbox, Form, Modal} from "antd"; +import { Button, Checkbox, Form, Modal } from "antd"; import queryString from "query-string"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useLocation, useNavigate} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {insertAuditTrail} from "../../redux/application/application.actions"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation, useNavigate } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setPartsOrderContext: (context) => - dispatch(setModalContext({context: context, modal: "partsOrder"})), - insertAuditTrail: ({jobid, operation, type}) => - dispatch(insertAuditTrail({jobid, operation, type })), + setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillDetailEditReturn); +export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn); -export function BillDetailEditReturn({ - setPartsOrderContext, - insertAuditTrail, - bodyshop, - data, - disabled, - }) { - const search = queryString.parse(useLocation().search); - const history = useNavigate(); - const {t} = useTranslation(); - const [form] = Form.useForm(); - const [open, setOpen] = useState(false); +export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) { + const search = queryString.parse(useLocation().search); + const history = useNavigate(); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [open, setOpen] = useState(false); - const handleFinish = ({billlines}) => { - const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); + const handleFinish = ({ billlines }) => { + const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); - setPartsOrderContext({ - actions: {}, - context: { - jobId: data.bills_by_pk.jobid, - vendorId: data.bills_by_pk.vendorid, - returnFromBill: data.bills_by_pk.id, - invoiceNumber: data.bills_by_pk.invoice_number, - linesToOrder: data.bills_by_pk.billlines - .filter((l) => selectedLines.includes(l.id)) - .map((i) => { - return { - line_desc: i.line_desc, - // db_price: i.actual_price, - act_price: i.actual_price, - cost: i.actual_cost, - quantity: i.quantity, - joblineid: i.joblineid, - oem_partno: i.jobline && i.jobline.oem_partno, - part_type: i.jobline && i.jobline.part_type, - }; - }), - isReturn: true, - }, - }); - delete search.billid; + setPartsOrderContext({ + actions: {}, + context: { + jobId: data.bills_by_pk.jobid, + vendorId: data.bills_by_pk.vendorid, + returnFromBill: data.bills_by_pk.id, + invoiceNumber: data.bills_by_pk.invoice_number, + linesToOrder: data.bills_by_pk.billlines + .filter((l) => selectedLines.includes(l.id)) + .map((i) => { + return { + line_desc: i.line_desc, + // db_price: i.actual_price, + act_price: i.actual_price, + cost: i.actual_cost, + quantity: i.quantity, + joblineid: i.joblineid, + oem_partno: i.jobline && i.jobline.oem_partno, + part_type: i.jobline && i.jobline.part_type + }; + }), + isReturn: true + } + }); + delete search.billid; - history({search: queryString.stringify(search)}); - setOpen(false); - }; - useEffect(() => { - if (open === false) form.resetFields(); - }, [open, form]); + history({ search: queryString.stringify(search) }); + setOpen(false); + }; + useEffect(() => { + if (open === false) form.resetFields(); + }, [open, form]); - return ( - <> - setOpen(false)} - destroyOnClose - title={t("bills.actions.return")} - onOk={() => form.submit()} - > - - - {(fields, {add, remove, move}) => { - return ( - - - - - { - form.setFieldsValue({ - billlines: form - .getFieldsValue() - .billlines.map((b) => ({ - ...b, - selected: e.target.checked, - })), - }); - }} - /> - - {t("billlines.fields.line_desc")} - {t("billlines.fields.quantity")} - {t("billlines.fields.actual_price")} - {t("billlines.fields.actual_cost")} - - - - {fields.map((field, index) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - - - ); - }} - - - - { - setOpen(true); - }} - > - {t("bills.actions.return")} - - > - ); + return ( + <> + setOpen(false)} + destroyOnClose + title={t("bills.actions.return")} + onOk={() => form.submit()} + > + + + {(fields, { add, remove, move }) => { + return ( + + + + + { + form.setFieldsValue({ + billlines: form.getFieldsValue().billlines.map((b) => ({ + ...b, + selected: e.target.checked + })) + }); + }} + /> + + {t("billlines.fields.line_desc")} + {t("billlines.fields.quantity")} + {t("billlines.fields.actual_price")} + {t("billlines.fields.actual_cost")} + + + + {fields.map((field, index) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + + + ); + }} + + + + { + setOpen(true); + }} + > + {t("bills.actions.return")} + + > + ); } diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx index ab7e9bc20..3cebb3e07 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx @@ -1,40 +1,38 @@ -import {Drawer, Grid} from "antd"; +import { Drawer, Grid } from "antd"; import queryString from "query-string"; import React from "react"; -import {useLocation, useNavigate} from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import BillDetailEditComponent from "./bill-detail-edit-component"; export default function BillDetailEditcontainer() { - const search = queryString.parse(useLocation().search); - const history = useNavigate(); + const search = queryString.parse(useLocation().search); + const history = useNavigate(); - const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) - .filter((screen) => !!screen[1]) - .slice(-1)[0]; + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; - const bpoints = { - xs: "100%", - sm: "100%", - md: "100%", - lg: "100%", - xl: "90%", - xxl: "90%", - }; - const drawerPercentage = selectedBreakpoint - ? bpoints[selectedBreakpoint[0]] - : "100%"; + const bpoints = { + xs: "100%", + sm: "100%", + md: "100%", + lg: "100%", + xl: "90%", + xxl: "90%" + }; + const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%"; - return ( - { - delete search.billid; - history({search: queryString.stringify(search)}); - }} - destroyOnClose - open={search.billid} - > - - - ); + return ( + { + delete search.billid; + history({ search: queryString.stringify(search) }); + }} + destroyOnClose + open={search.billid} + > + + + ); } diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index c93a4a0be..18d6db174 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -1,471 +1,426 @@ -import {useApolloClient, useMutation} from "@apollo/client"; -import {useSplitTreatments} from "@splitsoftware/splitio-react"; -import {Button, Checkbox, Form, Modal, notification, Space} from "antd"; +import { useApolloClient, useMutation } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { Button, Checkbox, Form, Modal, notification, Space } from "antd"; import _ from "lodash"; -import React, {useEffect, useMemo, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {INSERT_NEW_BILL} from "../../graphql/bills.queries"; -import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries"; -import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries"; -import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries"; -import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries"; -import {insertAuditTrail} from "../../redux/application/application.actions"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectBillEnterModal} from "../../redux/modals/modals.selectors"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { INSERT_NEW_BILL } from "../../graphql/bills.queries"; +import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries"; +import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; +import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries"; +import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; import confirmDialog from "../../utils/asyncConfirm"; import useLocalStorage from "../../utils/useLocalStorage"; import BillFormContainer from "../bill-form/bill-form.container"; -import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility"; -import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility"; -import {handleUpload} from "../documents-upload/documents-upload.utility"; +import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; +import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility"; +import { handleUpload } from "../documents-upload/documents-upload.utility"; const mapStateToProps = createStructuredSelector({ - billEnterModal: selectBillEnterModal, - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, + billEnterModal: selectBillEnterModal, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), - insertAuditTrail: ({jobid, billid, operation, type}) => - dispatch(insertAuditTrail({jobid, billid, operation, type })), + toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), + insertAuditTrail: ({ jobid, billid, operation, type }) => + dispatch(insertAuditTrail({ jobid, billid, operation, type })) }); const Templates = TemplateList("job_special"); -function BillEnterModalContainer({ - billEnterModal, - toggleModalVisible, - bodyshop, - currentUser, - insertAuditTrail, - }) { - const [form] = Form.useForm(); - const {t} = useTranslation(); - const [enterAgain, setEnterAgain] = useState(false); - const [insertBill] = useMutation(INSERT_NEW_BILL); - const [updateJobLines] = useMutation(UPDATE_JOB_LINE); - const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED); - const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); - const [loading, setLoading] = useState(false); - const client = useApolloClient(); - const [generateLabel, setGenerateLabel] = useLocalStorage( - "enter_bill_generate_label", - false - ); +function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) { + const [form] = Form.useForm(); + const { t } = useTranslation(); + const [enterAgain, setEnterAgain] = useState(false); + const [insertBill] = useMutation(INSERT_NEW_BILL); + const [updateJobLines] = useMutation(UPDATE_JOB_LINE); + const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED); + const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); + const [loading, setLoading] = useState(false); + const client = useApolloClient(); + const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false); - const {treatments: {Enhanced_Payroll}} = useSplitTreatments({ - attributes: {}, - names: ["Enhanced_Payroll"], - splitKey: bodyshop.imexshopid, + const { + treatments: { Enhanced_Payroll } + } = useSplitTreatments({ + attributes: {}, + names: ["Enhanced_Payroll"], + splitKey: bodyshop.imexshopid + }); + + const formValues = useMemo(() => { + return { + ...billEnterModal.context.bill, + //Added as a part of IO-2436 for capturing parts price changes. + billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({ + ...line, + original_actual_price: line.actual_price + })), + jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null, + federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0, + state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0, + local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0 + }; + }, [billEnterModal, bodyshop]); + + const handleFinish = async (values) => { + let totals = CalculateBillTotal(values); + if (totals.discrepancy.getAmount() !== 0) { + if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) { + return; + } + } + + setLoading(true); + const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values; + + let adjustmentsToInsert = {}; + let payrollAdjustmentsToInsert = []; + + const r1 = await insertBill({ + variables: { + bill: [ + { + ...remainingValues, + billlines: { + data: + remainingValues.billlines && + remainingValues.billlines.map((i) => { + const { + deductedfromlbr, + lbr_adjustment, + location: lineLocation, + part_type, + create_ppc, + original_actual_price, + ...restI + } = i; + + if (Enhanced_Payroll.treatment === "on") { + if ( + deductedfromlbr && + true //payroll is on + ) { + payrollAdjustmentsToInsert.push({ + id: i.joblineid, + convertedtolbr: true, + convertedtolbr_data: { + mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1, + mod_lbr_ty: lbr_adjustment.mod_lbr_ty + } + }); + } + } else { + if (deductedfromlbr) { + adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] = + (adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) - + restI.actual_price / lbr_adjustment.rate; + } + } + + return { + ...restI, + deductedfromlbr: deductedfromlbr, + lbr_adjustment, + joblineid: i.joblineid === "noline" ? null : i.joblineid, + applicable_taxes: { + federal: (i.applicable_taxes && i.applicable_taxes.federal) || false, + state: (i.applicable_taxes && i.applicable_taxes.state) || false, + local: (i.applicable_taxes && i.applicable_taxes.local) || false + } + }; + }) + } + } + ] + }, + refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"], + awaitRefetchQueries: true }); - const formValues = useMemo(() => { - return { - ...billEnterModal.context.bill, - //Added as a part of IO-2436 for capturing parts price changes. - billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({ - ...line, - original_actual_price: line.actual_price, - })), - jobid: - (billEnterModal.context.job && billEnterModal.context.job.id) || null, - federal_tax_rate: - (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || - 0, - state_tax_rate: - (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || - 0, - local_tax_rate: - (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || - 0, - }; - }, [billEnterModal, bodyshop]); - - const handleFinish = async (values) => { - let totals = CalculateBillTotal(values); - if (totals.discrepancy.getAmount() !== 0) { - if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) { - return; + await Promise.all( + payrollAdjustmentsToInsert.map((li) => { + return updateJobLines({ + variables: { + lineId: li.id, + line: { + convertedtolbr: li.convertedtolbr, + convertedtolbr_data: li.convertedtolbr_data } - } - - setLoading(true); - const { - upload, - location, - outstanding_returns, - inventory, - federal_tax_exempt, - ...remainingValues - } = values; - - let adjustmentsToInsert = {}; - let payrollAdjustmentsToInsert = []; - - const r1 = await insertBill({ - variables: { - bill: [ - { - ...remainingValues, - billlines: { - data: - remainingValues.billlines && - remainingValues.billlines.map((i) => { - const { - deductedfromlbr, - lbr_adjustment, - location: lineLocation, - part_type, - create_ppc, - original_actual_price, - ...restI - } = i; - - if (Enhanced_Payroll.treatment === "on") { - if ( - deductedfromlbr && - true //payroll is on - ) { - payrollAdjustmentsToInsert.push({ - id: i.joblineid, - convertedtolbr: true, - convertedtolbr_data: { - mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1, - mod_lbr_ty: lbr_adjustment.mod_lbr_ty, - }, - }); - } - } else { - if (deductedfromlbr) { - adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] = - (adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) - - restI.actual_price / lbr_adjustment.rate; - } - } - - return { - ...restI, - deductedfromlbr: deductedfromlbr, - lbr_adjustment, - joblineid: i.joblineid === "noline" ? null : i.joblineid, - applicable_taxes: { - federal: - (i.applicable_taxes && i.applicable_taxes.federal) || - false, - state: - (i.applicable_taxes && i.applicable_taxes.state) || - false, - local: - (i.applicable_taxes && i.applicable_taxes.local) || - false, - }, - }; - }), - }, - }, - ], - }, - refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"], - awaitRefetchQueries: true + } }); + }) + ); - await Promise.all( - payrollAdjustmentsToInsert.map((li) => { - return updateJobLines({ - variables: { - lineId: li.id, - line: { - convertedtolbr: li.convertedtolbr, - convertedtolbr_data: li.convertedtolbr_data, - }, - }, - }); - }) - ); - - const adjKeys = Object.keys(adjustmentsToInsert); - if (adjKeys.length > 0) { - //Query the adjustments, merge, and update them. - const existingAdjustments = await client.query({ - query: QUERY_JOB_LBR_ADJUSTMENTS, - variables: { - id: values.jobid, - }, - }); - - const newAdjustments = _.cloneDeep( - existingAdjustments.data.jobs_by_pk.lbr_adjustments - ); - - adjKeys.forEach((key) => { - newAdjustments[key] = - (newAdjustments[key] || 0) + adjustmentsToInsert[key]; - - insertAuditTrail({ - jobid: values.jobid, - operation: AuditTrailMapping.jobmodifylbradj({ - mod_lbr_ty: key, - hours: adjustmentsToInsert[key].toFixed(1), - }), - type: "jobmodifylbradj",}); - }); - - const jobUpdate = client.mutate({ - mutation: UPDATE_JOB, - variables: { - jobId: values.jobid, - job: {lbr_adjustments: newAdjustments}, - }, - }); - if (!!jobUpdate.errors) { - notification["error"]({ - message: t("jobs.errors.saving", { - message: JSON.stringify(jobUpdate.errors), - }), - }); - return; - } + const adjKeys = Object.keys(adjustmentsToInsert); + if (adjKeys.length > 0) { + //Query the adjustments, merge, and update them. + const existingAdjustments = await client.query({ + query: QUERY_JOB_LBR_ADJUSTMENTS, + variables: { + id: values.jobid } + }); - const markPolReceived = - outstanding_returns && - outstanding_returns.filter((o) => o.cm_received === true); + const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments); - if (markPolReceived && markPolReceived.length > 0) { - const r2 = await updatePartsOrderLines({ - variables: {partsLineIds: markPolReceived.map((p) => p.id)}, - refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID" ], - }); - if (!!r2.errors) { - setLoading(false); - setEnterAgain(false); - notification["error"]({ - message: t("parts_orders.errors.updating", { - message: JSON.stringify(r2.errors), - }), - }); - } - } - - if (!!r1.errors) { - setLoading(false); - setEnterAgain(false); - notification["error"]({ - message: t("bills.errors.creating", { - message: JSON.stringify(r1.errors), - }), - }); - } - - const billId = r1.data.insert_bills.returning[0].id; - const markInventoryConsumed = - inventory && inventory.filter((i) => i.consumefrominventory); - - if (markInventoryConsumed && markInventoryConsumed.length > 0) { - const r2 = await updateInventoryLines({ - variables: { - InventoryIds: markInventoryConsumed.map((p) => p.id), - consumedbybillid: billId, - }, - }); - if (!!r2.errors) { - setLoading(false); - setEnterAgain(false); - notification["error"]({ - message: t("inventory.errors.updating", { - message: JSON.stringify(r2.errors), - }), - }); - } - } - //If it's not a credit memo, update the statuses. - - if (!values.is_credit_memo) { - await Promise.all( - remainingValues.billlines - .filter((il) => il.joblineid !== "noline") - .map((li) => { - return updateJobLines({ - variables: { - lineId: li.joblineid, - line: { - location: li.location || location, - status: - bodyshop.md_order_statuses.default_received || "Received*", - //Added parts price changes. - ...(li.create_ppc && - li.original_actual_price !== li.actual_price - ? { - act_price_before_ppc: li.original_actual_price, - act_price: li.actual_price, - } - : {}), - }, - }, - }); - }) - ); - } - - ///////////////////////// - if (upload && upload.length > 0) { - //insert Each of the documents? - - if (bodyshop.uselocalmediaserver) { - upload.forEach((u) => { - handleLocalUpload({ - ev: {file: u.originFileObj}, - context: { - jobid: values.jobid, - invoice_number: remainingValues.invoice_number, - vendorid: remainingValues.vendorid, - }, - }); - }); - } else { - upload.forEach((u) => { - handleUpload( - {file: u.originFileObj}, - { - bodyshop: bodyshop, - uploaded_by: currentUser.email, - jobId: values.jobid, - billId: billId, - tagsArray: null, - callback: null, - } - ); - }); - } - } - /////////////////////////// - setLoading(false); - notification["success"]({ - message: t("bills.successes.created"), - }); - - if (generateLabel) { - GenerateDocument( - { - name: Templates.parts_invoice_label_single.key, - variables: { - id: billId, - }, - }, - {}, - "p" - ); - } - - if (billEnterModal.actions.refetch) billEnterModal.actions.refetch(); + adjKeys.forEach((key) => { + newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key]; insertAuditTrail({ - jobid: values.jobid, - billid: billId, - operation: AuditTrailMapping.billposted( - r1.data.insert_bills.returning[0].invoice_number - ), - type: "billposted", + jobid: values.jobid, + operation: AuditTrailMapping.jobmodifylbradj({ + mod_lbr_ty: key, + hours: adjustmentsToInsert[key].toFixed(1) + }), + type: "jobmodifylbradj" + }); + }); + + const jobUpdate = client.mutate({ + mutation: UPDATE_JOB, + variables: { + jobId: values.jobid, + job: { lbr_adjustments: newAdjustments } + } + }); + if (!!jobUpdate.errors) { + notification["error"]({ + message: t("jobs.errors.saving", { + message: JSON.stringify(jobUpdate.errors) + }) + }); + return; + } + } + + const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true); + + if (markPolReceived && markPolReceived.length > 0) { + const r2 = await updatePartsOrderLines({ + variables: { partsLineIds: markPolReceived.map((p) => p.id) }, + refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"] + }); + if (!!r2.errors) { + setLoading(false); + setEnterAgain(false); + notification["error"]({ + message: t("parts_orders.errors.updating", { + message: JSON.stringify(r2.errors) + }) + }); + } + } + + if (!!r1.errors) { + setLoading(false); + setEnterAgain(false); + notification["error"]({ + message: t("bills.errors.creating", { + message: JSON.stringify(r1.errors) + }) + }); + } + + const billId = r1.data.insert_bills.returning[0].id; + const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory); + + if (markInventoryConsumed && markInventoryConsumed.length > 0) { + const r2 = await updateInventoryLines({ + variables: { + InventoryIds: markInventoryConsumed.map((p) => p.id), + consumedbybillid: billId + } + }); + if (!!r2.errors) { + setLoading(false); + setEnterAgain(false); + notification["error"]({ + message: t("inventory.errors.updating", { + message: JSON.stringify(r2.errors) + }) + }); + } + } + //If it's not a credit memo, update the statuses. + + if (!values.is_credit_memo) { + await Promise.all( + remainingValues.billlines + .filter((il) => il.joblineid !== "noline") + .map((li) => { + return updateJobLines({ + variables: { + lineId: li.joblineid, + line: { + location: li.location || location, + status: bodyshop.md_order_statuses.default_received || "Received*", + //Added parts price changes. + ...(li.create_ppc && li.original_actual_price !== li.actual_price + ? { + act_price_before_ppc: li.original_actual_price, + act_price: li.actual_price + } + : {}) + } + } + }); + }) + ); + } + + ///////////////////////// + if (upload && upload.length > 0) { + //insert Each of the documents? + + if (bodyshop.uselocalmediaserver) { + upload.forEach((u) => { + handleLocalUpload({ + ev: { file: u.originFileObj }, + context: { + jobid: values.jobid, + invoice_number: remainingValues.invoice_number, + vendorid: remainingValues.vendorid + } + }); + }); + } else { + upload.forEach((u) => { + handleUpload( + { file: u.originFileObj }, + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: values.jobid, + billId: billId, + tagsArray: null, + callback: null + } + ); + }); + } + } + /////////////////////////// + setLoading(false); + notification["success"]({ + message: t("bills.successes.created") }); - if (enterAgain) { - form.resetFields(); - form.setFieldsValue({ - ...formValues, - vendorid:values.vendorid, - billlines: [], - }); - // form.resetFields(); - } else { - toggleModalVisible(); - } - setEnterAgain(false); - }; + if (generateLabel) { + GenerateDocument( + { + name: Templates.parts_invoice_label_single.key, + variables: { + id: billId + } + }, + {}, + "p" + ); + } - const handleCancel = () => { - const r = window.confirm(t("general.labels.cancel")); - if (r === true) { - toggleModalVisible(); - } - }; + if (billEnterModal.actions.refetch) billEnterModal.actions.refetch(); - useEffect(() => { - if (enterAgain) form.submit(); - }, [enterAgain, form]); + insertAuditTrail({ + jobid: values.jobid, + billid: billId, + operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number), + type: "billposted" + }); - useEffect(() => { - if (billEnterModal.open) { - form.setFieldsValue(formValues); - } else { - form.resetFields(); - } - }, [billEnterModal.open, form, formValues]); + if (enterAgain) { + form.resetFields(); + form.setFieldsValue({ + ...formValues, + vendorid: values.vendorid, + billlines: [] + }); + // form.resetFields(); + } else { + toggleModalVisible(); + } + setEnterAgain(false); + }; - return ( - form.submit()} - onCancel={handleCancel} - afterClose={() => { - form.resetFields(); - setLoading(false); - }} - footer={ - - setGenerateLabel(e.target.checked)} - > - {t("bills.labels.generatepartslabel")} - - {t("general.actions.cancel")} - form.submit()}> - {t("general.actions.save")} - - {billEnterModal.context && billEnterModal.context.id ? null : ( - { - setEnterAgain(true); - }} - > - {t("general.actions.saveandnew")} - - )} - - } - destroyOnClose - > - { - setEnterAgain(false); - }} + const handleCancel = () => { + const r = window.confirm(t("general.labels.cancel")); + if (r === true) { + toggleModalVisible(); + } + }; + + useEffect(() => { + if (enterAgain) form.submit(); + }, [enterAgain, form]); + + useEffect(() => { + if (billEnterModal.open) { + form.setFieldsValue(formValues); + } else { + form.resetFields(); + } + }, [billEnterModal.open, form, formValues]); + + return ( + form.submit()} + onCancel={handleCancel} + afterClose={() => { + form.resetFields(); + setLoading(false); + }} + footer={ + + setGenerateLabel(e.target.checked)}> + {t("bills.labels.generatepartslabel")} + + {t("general.actions.cancel")} + form.submit()}> + {t("general.actions.save")} + + {billEnterModal.context && billEnterModal.context.id ? null : ( + { + setEnterAgain(true); + }} > - - - - ); + {t("general.actions.saveandnew")} + + )} + + } + destroyOnClose + > + { + setEnterAgain(false); + }} + > + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillEnterModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer); diff --git a/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx b/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx index ef54f443c..2cb93f328 100644 --- a/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx +++ b/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx @@ -1,136 +1,114 @@ -import {Form, Input, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import { Form, Input, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {alphaSort} from "../../utils/sorters"; +import { alphaSort } from "../../utils/sorters"; import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component"; -export default function BillFormLinesExtended({ - lineData, - discount, - form, - responsibilityCenters, - disabled, - }) { - const [search, setSearch] = useState(""); - const {t} = useTranslation(); - const columns = [ +export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) { + const [search, setSearch] = useState(""); + const { t } = useTranslation(); + const columns = [ + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + width: "10%", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc) + }, + { + title: t("joblines.fields.oem_partno"), + dataIndex: "oem_partno", + key: "oem_partno", + width: "10%", + sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno) + }, + { + title: t("joblines.fields.part_type"), + dataIndex: "part_type", + key: "part_type", + width: "10%", + filters: [ { - title: t("joblines.fields.line_desc"), - dataIndex: "line_desc", - key: "line_desc", - width: "10%", - sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + text: t("jobs.labels.partsfilter"), + value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"] }, { - title: t("joblines.fields.oem_partno"), - dataIndex: "oem_partno", - key: "oem_partno", - width: "10%", - sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno), + text: t("joblines.fields.part_types.PAN"), + value: ["PAN", "PAP"] }, { - title: t("joblines.fields.part_type"), - dataIndex: "part_type", - key: "part_type", - width: "10%", - filters: [ - { - text: t("jobs.labels.partsfilter"), - value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"], - }, - { - text: t("joblines.fields.part_types.PAN"), - value: ["PAN", "PAP"], - }, - { - text: t("joblines.fields.part_types.PAL"), - value: ["PAL"], - }, - { - text: t("joblines.fields.part_types.PAA"), - value: ["PAA"], - }, - { - text: t("joblines.fields.part_types.PAS"), - value: ["PAS", "PASL"], - }, - ], - onFilter: (value, record) => value.includes(record.part_type), - render: (text, record) => - record.part_type - ? t(`joblines.fields.part_types.${record.part_type}`) - : null, + text: t("joblines.fields.part_types.PAL"), + value: ["PAL"] }, + { + text: t("joblines.fields.part_types.PAA"), + value: ["PAA"] + }, + { + text: t("joblines.fields.part_types.PAS"), + value: ["PAS", "PASL"] + } + ], + onFilter: (value, record) => value.includes(record.part_type), + render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null) + }, - { - title: t("joblines.fields.act_price"), - dataIndex: "act_price", - key: "act_price", - width: "10%", - sorter: (a, b) => a.act_price - b.act_price, - shouldCellUpdate: false, - render: (text, record) => ( - <> - - {record.db_ref === "900510" || record.db_ref === "900511" - ? record.prt_dsmk_m - : record.act_price} - - {record.part_qty ? `(x ${record.part_qty})` : null} - {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( - {`(${record.prt_dsmk_p}%)`} - ) : ( - <>> - )} - > - ), - }, - { - title: t("billlines.fields.posting"), - dataIndex: "posting", - key: "posting", + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + width: "10%", + sorter: (a, b) => a.act_price - b.act_price, + shouldCellUpdate: false, + render: (text, record) => ( + <> + + {record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price} + + {record.part_qty ? `(x ${record.part_qty})` : null} + {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( + {`(${record.prt_dsmk_p}%)`} + ) : ( + <>> + )} + > + ) + }, + { + title: t("billlines.fields.posting"), + dataIndex: "posting", + key: "posting", - render: (text, record, index) => ( - - - - ), - }, - ]; - - const data = - search === "" - ? lineData - : lineData.filter( - (l) => - (l.line_desc && - l.line_desc.toLowerCase().includes(search.toLowerCase())) || - (l.oem_partno && - l.oem_partno.toLowerCase().includes(search.toLowerCase())) || - (l.act_price && - l.act_price.toString().startsWith(search.toString())) - ); - - return ( - - console.log(form.getFieldsValue())}>form - setSearch(e.target.value)} allowClear/> - + render: (text, record, index) => ( + + - ); + ) + } + ]; + + const data = + search === "" + ? lineData + : lineData.filter( + (l) => + (l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) || + (l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) || + (l.act_price && l.act_price.toString().startsWith(search.toString())) + ); + + return ( + + console.log(form.getFieldsValue())}>form + setSearch(e.target.value)} allowClear /> + + + ); } diff --git a/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx b/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx index 2761a20e1..50dd3ab10 100644 --- a/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx +++ b/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx @@ -1,284 +1,216 @@ import React from "react"; -import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons"; -import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd"; -import {useTranslation} from "react-i18next"; +import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons"; +import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd"; +import { useTranslation } from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CiecaSelect from "../../utils/Ciecaselect"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillFormItemsExtendedFormItem); +export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem); export function BillFormItemsExtendedFormItem({ - value, - bodyshop, - form, - record, - index, - disabled, - responsibilityCenters, - discount, - }) { - // const { billlineskeys } = form.getFieldsValue("billlineskeys"); - - const {t} = useTranslation(); - if (!value) - return ( - { - const values = form.getFieldsValue("billlineskeys"); - - form.setFieldsValue({ - ...values, - billlineskeys: { - ...(values.billlineskeys || {}), - [record.id]: { - joblineid: record.id, - line_desc: record.line_desc, - quantity: record.part_qty || 1, - actual_price: record.act_price, - cost_center: record.part_type - ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid - ? record.part_type - : responsibilityCenters.defaults && - (responsibilityCenters.defaults.costs[record.part_type] || - null) - : null, - }, - }, - }); - }} - > - - - ); + value, + bodyshop, + form, + record, + index, + disabled, + responsibilityCenters, + discount +}) { + // const { billlineskeys } = form.getFieldsValue("billlineskeys"); + const { t } = useTranslation(); + if (!value) return ( - - - - - - - - - { - const {billlineskeys} = form.getFieldsValue("billlineskeys"); - form.setFieldsValue({ - billlineskeys: { - ...billlineskeys, - [record.id]: { - ...billlineskeys[billlineskeys], - actual_cost: !!billlineskeys[billlineskeys].actual_cost - ? billlineskeys[billlineskeys].actual_cost - : Math.round( - (parseFloat(e.target.value) * (1 - discount) + - Number.EPSILON) * - 100 - ) / 100, - }, - }, - }); - }} - /> - - - - - - {() => { - const line = value; - if (!!!line) return null; - const lineDiscount = ( - 1 - - Math.round((line.actual_cost / line.actual_price) * 100) / 100 - ).toPrecision(2); + { + const values = form.getFieldsValue("billlineskeys"); - if (lineDiscount - discount === 0) return ; - return ; - }} - - - - {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber - ? CiecaSelect(true, false) - : responsibilityCenters.costs.map((item) => ( - {item.name} - ))} - - - - - {bodyshop.md_parts_locations.map((loc, idx) => ( - - {loc} - - ))} - - - - - - - {() => { - if ( - form.getFieldsValue("billlineskeys").billlineskeys[record.id] - .deductedfromlbr - ) - return ( - - - - - {t("joblines.fields.lbr_types.LAA")} - - - {t("joblines.fields.lbr_types.LAB")} - - - {t("joblines.fields.lbr_types.LAD")} - - - {t("joblines.fields.lbr_types.LAE")} - - - {t("joblines.fields.lbr_types.LAF")} - - - {t("joblines.fields.lbr_types.LAG")} - - - {t("joblines.fields.lbr_types.LAM")} - - - {t("joblines.fields.lbr_types.LAR")} - - - {t("joblines.fields.lbr_types.LAS")} - - - {t("joblines.fields.lbr_types.LAU")} - - - {t("joblines.fields.lbr_types.LA1")} - - - {t("joblines.fields.lbr_types.LA2")} - - - {t("joblines.fields.lbr_types.LA3")} - - - {t("joblines.fields.lbr_types.LA4")} - - - - - - - - ); - return <>>; - }} - - - - - - - - - - - - - { - const values = form.getFieldsValue("billlineskeys"); - - form.setFieldsValue({ - ...values, - billlineskeys: { - ...(values.billlineskeys || {}), - [record.id]: null, - }, - }); - }} - > - - - + form.setFieldsValue({ + ...values, + billlineskeys: { + ...(values.billlineskeys || {}), + [record.id]: { + joblineid: record.id, + line_desc: record.line_desc, + quantity: record.part_qty || 1, + actual_price: record.act_price, + cost_center: record.part_type + ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid + ? record.part_type + : responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null) + : null + } + } + }); + }} + > + + ); + + return ( + + + + + + + + + { + const { billlineskeys } = form.getFieldsValue("billlineskeys"); + form.setFieldsValue({ + billlineskeys: { + ...billlineskeys, + [record.id]: { + ...billlineskeys[billlineskeys], + actual_cost: !!billlineskeys[billlineskeys].actual_cost + ? billlineskeys[billlineskeys].actual_cost + : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100 + } + } + }); + }} + /> + + + + + + {() => { + const line = value; + if (!!!line) return null; + const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2); + + if (lineDiscount - discount === 0) return ; + return ; + }} + + + + {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber + ? CiecaSelect(true, false) + : responsibilityCenters.costs.map((item) => {item.name})} + + + + + {bodyshop.md_parts_locations.map((loc, idx) => ( + + {loc} + + ))} + + + + + + + {() => { + if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr) + return ( + + + + {t("joblines.fields.lbr_types.LAA")} + {t("joblines.fields.lbr_types.LAB")} + {t("joblines.fields.lbr_types.LAD")} + {t("joblines.fields.lbr_types.LAE")} + {t("joblines.fields.lbr_types.LAF")} + {t("joblines.fields.lbr_types.LAG")} + {t("joblines.fields.lbr_types.LAM")} + {t("joblines.fields.lbr_types.LAR")} + {t("joblines.fields.lbr_types.LAS")} + {t("joblines.fields.lbr_types.LAU")} + {t("joblines.fields.lbr_types.LA1")} + {t("joblines.fields.lbr_types.LA2")} + {t("joblines.fields.lbr_types.LA3")} + {t("joblines.fields.lbr_types.LA4")} + + + + + + + ); + return <>>; + }} + + + + + + + + + + + + + { + const values = form.getFieldsValue("billlineskeys"); + + form.setFieldsValue({ + ...values, + billlineskeys: { + ...(values.billlineskeys || {}), + [record.id]: null + } + }); + }} + > + + + + ); } diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index fcf167d73..508be6af5 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -1,30 +1,30 @@ -import Icon, { UploadOutlined } from '@ant-design/icons'; -import { useApolloClient } from '@apollo/client'; -import { useSplitTreatments } from '@splitsoftware/splitio-react'; -import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { MdOpenInNew } from 'react-icons/md'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { createStructuredSelector } from 'reselect'; -import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries'; -import { selectBodyshop } from '../../redux/user/user.selectors'; -import dayjs from '../../utils/day'; -import InstanceRenderManager from '../../utils/instanceRenderMgr'; -import AlertComponent from '../alert/alert.component'; -import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component'; -import FormDatePicker from '../form-date-picker/form-date-picker.component'; -import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component'; -import CurrencyInput from '../form-items-formatted/currency-form-item.component'; -import JobSearchSelect from '../job-search-select/job-search-select.component'; -import LayoutFormRow from '../layout-form-row/layout-form-row.component'; -import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component'; -import BillFormLines from './bill-form.lines.component'; -import { CalculateBillTotal } from './bill-form.totals.utility'; +import Icon, { UploadOutlined } from "@ant-design/icons"; +import { useApolloClient } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MdOpenInNew } from "react-icons/md"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import dayjs from "../../utils/day"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import AlertComponent from "../alert/alert.component"; +import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; +import FormDatePicker from "../form-date-picker/form-date-picker.component"; +import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; +import CurrencyInput from "../form-items-formatted/currency-form-item.component"; +import JobSearchSelect from "../job-search-select/job-search-select.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; +import BillFormLines from "./bill-form.lines.component"; +import { CalculateBillTotal } from "./bill-form.totals.utility"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({}); @@ -41,18 +41,18 @@ export function BillFormComponent({ job, loadOutstandingReturns, loadInventory, - preferredMake, + preferredMake }) { const { t } = useTranslation(); const client = useApolloClient(); const [discount, setDiscount] = useState(0); const { - treatments: { Extended_Bill_Posting, ClosingPeriod }, + treatments: { Extended_Bill_Posting, ClosingPeriod } } = useSplitTreatments({ attributes: {}, - names: ['Extended_Bill_Posting', 'ClosingPeriod'], - splitKey: bodyshop.imexshopid, + names: ["Extended_Bill_Posting", "ClosingPeriod"], + splitKey: bodyshop.imexshopid }); const handleVendorSelect = (props, opt) => { @@ -62,16 +62,16 @@ export function BillFormComponent({ !billEdit && loadOutstandingReturns({ variables: { - jobId: form.getFieldValue('jobid'), - vendorId: opt.value, - }, + jobId: form.getFieldValue("jobid"), + vendorId: opt.value + } }); }; const handleFederalTaxExemptSwitchToggle = (checked) => { // Early gate if (!checked) return; - const values = form.getFieldsValue('billlines'); + const values = form.getFieldsValue("billlines"); // Gate bill lines if (!values?.billlines?.length) return; @@ -83,26 +83,26 @@ export function BillFormComponent({ }; useEffect(() => { - if (job) form.validateFields(['is_credit_memo']); + if (job) form.validateFields(["is_credit_memo"]); }, [job, form]); useEffect(() => { - const vendorId = form.getFieldValue('vendorid'); + const vendorId = form.getFieldValue("vendorid"); if (vendorId && vendorAutoCompleteOptions) { const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId); if (matchingVendors.length === 1) { setDiscount(matchingVendors[0].discount); } } - const jobId = form.getFieldValue('jobid'); + const jobId = form.getFieldValue("jobid"); if (jobId) { loadLines({ variables: { id: jobId } }); - if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) { + if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) { loadOutstandingReturns({ variables: { jobId: jobId, - vendorId: vendorId, - }, + vendorId: vendorId + } }); } } @@ -118,24 +118,24 @@ export function BillFormComponent({ setDiscount, vendorAutoCompleteOptions, loadLines, - bodyshop.inhousevendorid, + bodyshop.inhousevendorid ]); return ( - + { - if ( - form.getFieldValue('jobid') !== null && - form.getFieldValue('jobid') !== undefined - ) { - loadLines({ variables: { id: form.getFieldValue('jobid') } }); - if ( - form.getFieldValue('vendorid') !== null && - form.getFieldValue('vendorid') !== undefined - ) { + if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) { + loadLines({ variables: { id: form.getFieldValue("jobid") } }); + if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) { loadOutstandingReturns({ variables: { - jobId: form.getFieldValue('jobid'), - vendorId: form.getFieldValue('vendorid'), - }, + jobId: form.getFieldValue("jobid"), + vendorId: form.getFieldValue("vendorid") + } }); } } @@ -164,22 +158,22 @@ export function BillFormComponent({ /> ({ validator(rule, value) { - if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) { - return Promise.reject(t('bills.validation.manualinhouse')); + if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) { + return Promise.reject(t("bills.validation.manualinhouse")); } return Promise.resolve(); - }, - }), + } + }) ]} > - {t('bills.labels.iouexists')} - + {t("bills.labels.iouexists")} + {iou.ro_number} @@ -216,89 +206,85 @@ export function BillFormComponent({ ))} ({ async validator(rule, value) { - const vendorid = getFieldValue('vendorid'); + const vendorid = getFieldValue("vendorid"); if (vendorid && value) { const response = await client.query({ query: CHECK_BILL_INVOICE_NUMBER, variables: { invoice_number: value, - vendorid: vendorid, - }, + vendorid: vendorid + } }); if (response.data.bills_aggregate.aggregate.count === 0) { return Promise.resolve(); } else if ( response.data.bills_aggregate.nodes.length === 1 && - response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id') + response.data.bills_aggregate.nodes[0].id === form.getFieldValue("id") ) { return Promise.resolve(); } - return Promise.reject(t('bills.validation.unique_invoice_number')); + return Promise.reject(t("bills.validation.unique_invoice_number")); } else { return Promise.resolve(); } - }, - }), + } + }) ]} > ({ validator(rule, value) { - if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) { + if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) { if ( dayjs(value) - .startOf('day') - .isSameOrAfter( - dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day') - ) && + .startOf("day") + .isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) && dayjs(value) - .startOf('day') - .isSameOrBefore( - dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day') - ) + .startOf("day") + .isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day")) ) { return Promise.resolve(); } else { - return Promise.reject(t('bills.validation.closingperiod')); + return Promise.reject(t("bills.validation.closingperiod")); } } else { return Promise.resolve(); } - }, - }), + } + }) ]} > ({ validator(rule, value) { - if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) { + if (value === true && getFieldValue("jobid") && getFieldValue("vendorid")) { //Removed as this would cause an additional reload when validating the form on submit and clear the values. // loadOutstandingReturns({ // variables: { @@ -316,31 +302,31 @@ export function BillFormComponent({ job.status === bodyshop.md_ro_statuses.default_void) && (value === false || !value) ) { - return Promise.reject(t('bills.labels.onlycmforinvoiced')); + return Promise.reject(t("bills.labels.onlycmforinvoiced")); } return Promise.resolve(); - }, - }), + } + }) ]} > {!billEdit && ( - - + + {bodyshop.md_parts_locations.map((loc, idx) => ( {loc} @@ -353,40 +339,36 @@ export function BillFormComponent({ {InstanceRenderManager({ imex: ( - + - ), + ) })} - + {InstanceRenderManager({ imex: ( <> - + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( - + ) : null} > - ), + ) })} {() => { const values = form.getFieldsValue([ - 'billlines', - 'total', - 'federal_tax_rate', - 'state_tax_rate', - 'local_tax_rate', + "billlines", + "total", + "federal_tax_rate", + "state_tax_rate", + "local_tax_rate" ]); let totals; if (!!values.total && !!values.billlines && values.billlines.length > 0) @@ -394,56 +376,48 @@ export function BillFormComponent({ if (!!totals) return ( - - + + {InstanceRenderManager({ imex: ( - ), + ) })} - + {InstanceRenderManager({ imex: ( - ), + ) })} - {form.getFieldValue('is_credit_memo') ? ( - + {form.getFieldValue("is_credit_memo") ? ( + ) : null} ); @@ -451,9 +425,9 @@ export function BillFormComponent({ }} - {t('bills.labels.bill_lines')} + {t("bills.labels.bill_lines")} - {Extended_Bill_Posting.treatment === 'on' ? ( + {Extended_Bill_Posting.treatment === "on" ? ( )} - - {t('documents.labels.upload')} + + {t("documents.labels.upload")} { if (Array.isArray(e)) { diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx index 67b3c9ab3..e24418b12 100644 --- a/client/src/components/bill-form/bill-form.container.jsx +++ b/client/src/components/bill-form/bill-form.container.jsx @@ -1,83 +1,67 @@ -import {useLazyQuery, useQuery} from "@apollo/client"; -import {useSplitTreatments} from "@splitsoftware/splitio-react"; +import { useLazyQuery, useQuery } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries"; -import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries"; -import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries"; -import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries"; +import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries"; +import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries"; +import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component"; import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component"; import BillFormComponent from "./bill-form.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); -export function BillFormContainer({ - bodyshop, - form, - billEdit, - disabled, - disableInvNumber, - }) { - const {treatments: {Simple_Inventory}} = useSplitTreatments({ - attributes: {}, - names: ["Simple_Inventory"], - splitKey: bodyshop && bodyshop.imexshopid, - }); +export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) { + const { + treatments: { Simple_Inventory } + } = useSplitTreatments({ + attributes: {}, + names: ["Simple_Inventory"], + splitKey: bodyshop && bodyshop.imexshopid + }); - const {data: VendorAutoCompleteData} = useQuery( - SEARCH_VENDOR_AUTOCOMPLETE, - {fetchPolicy: "network-only", nextFetchPolicy: "network-only"} - ); + const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - const [loadLines, {data: lineData}] = useLazyQuery( - GET_JOB_LINES_TO_ENTER_BILL - ); + const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL); - const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] = - useLazyQuery(QUERY_UNRECEIVED_LINES); - const [loadInventory, {loading: inventoryLoading, data: inventoryData}] = - useLazyQuery(QUERY_OUTSTANDING_INVENTORY); + const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES); + const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY); - return ( - <> - - {!billEdit && ( - - )} - {Simple_Inventory.treatment === "on" && ( - - )} - > - ); + return ( + <> + + {!billEdit && } + {Simple_Inventory.treatment === "on" && ( + + )} + > + ); } export default connect(mapStateToProps, null)(BillFormContainer); diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index ee02d3acc..555be39e3 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -1,17 +1,6 @@ import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { - Button, - Checkbox, - Form, - Input, - InputNumber, - Select, - Space, - Switch, - Table, - Tooltip, -} from "antd"; +import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -24,889 +13,641 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component" import InstanceRenderManager from "../../utils/instanceRenderMgr"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export function BillEnterModalLinesComponent({ - bodyshop, - disabled, - lineData, - discount, - form, - responsibilityCenters, - billEdit, - billid, + bodyshop, + disabled, + lineData, + discount, + form, + responsibilityCenters, + billEdit, + billid }) { - const { t } = useTranslation(); - const { setFieldsValue, getFieldsValue, getFieldValue } = form; + const { t } = useTranslation(); + const { setFieldsValue, getFieldsValue, getFieldValue } = form; - const { - treatments: { Simple_Inventory, Enhanced_Payroll }, - } = useSplitTreatments({ - attributes: {}, - names: ["Simple_Inventory", "Enhanced_Payroll"], - splitKey: bodyshop && bodyshop.imexshopid, + const { + treatments: { Simple_Inventory, Enhanced_Payroll } + } = useSplitTreatments({ + attributes: {}, + names: ["Simple_Inventory", "Enhanced_Payroll"], + splitKey: bodyshop && bodyshop.imexshopid + }); + + const columns = (remove) => { + return [ + { + title: t("billlines.fields.jobline"), + dataIndex: "joblineid", + editable: true, + width: "20rem", + formItemProps: (field) => { + return { + key: `${field.index}joblinename`, + name: [field.name, "joblineid"], + label: t("billlines.fields.jobline"), + rules: [ + { + required: true + //message: t("general.validation.required"), + } + ] + }; + }, + wrapper: (props) => ( + prev.is_credit_memo !== cur.is_credit_memo}> + {() => { + return props.children; + }} + + ), + formInput: (record, index) => ( + { + setFieldsValue({ + billlines: getFieldsValue(["billlines"]).billlines.map((item, idx) => { + if (idx === index) { + return { + ...item, + line_desc: opt.line_desc, + quantity: opt.part_qty || 1, + actual_price: opt.cost, + original_actual_price: opt.cost, + cost_center: opt.part_type + ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid + ? opt.part_type !== "PAE" + ? opt.part_type + : null + : responsibilityCenters.defaults && + (responsibilityCenters.defaults.costs[opt.part_type] || null) + : null + }; + } + return item; + }) + }); + }} + /> + ) + }, + { + title: t("billlines.fields.line_desc"), + dataIndex: "line_desc", + editable: true, + + formItemProps: (field) => { + return { + key: `${field.index}line_desc`, + name: [field.name, "line_desc"], + label: t("billlines.fields.line_desc"), + rules: [ + { + required: true + //message: t("general.validation.required"), + } + ] + }; + }, + formInput: (record, index) => + }, + { + title: t("billlines.fields.quantity"), + dataIndex: "quantity", + editable: true, + width: "4rem", + formItemProps: (field) => { + return { + key: `${field.index}quantity`, + name: [field.name, "quantity"], + label: t("billlines.fields.quantity"), + rules: [ + { + required: true + //message: t("general.validation.required"), + }, + ({ getFieldValue }) => ({ + validator(rule, value) { + if (value && getFieldValue("billlines")[field.fieldKey]?.inventories?.length > value) { + return Promise.reject( + t("bills.validation.inventoryquantity", { + number: getFieldValue("billlines")[field.fieldKey]?.inventories?.length + }) + ); + } + return Promise.resolve(); + } + }) + ] + }; + }, + formInput: (record, index) => + }, + { + title: t("billlines.fields.actual_price"), + dataIndex: "actual_price", + width: "8rem", + editable: true, + formItemProps: (field) => { + return { + key: `${field.index}actual_price`, + name: [field.name, "actual_price"], + label: t("billlines.fields.actual_price"), + rules: [ + { + required: true + //message: t("general.validation.required"), + } + ] + }; + }, + formInput: (record, index) => ( + { + setFieldsValue({ + billlines: getFieldsValue("billlines").billlines.map((item, idx) => { + if (idx === index) { + return { + ...item, + actual_cost: !!item.actual_cost + ? item.actual_cost + : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100 + }; + } + return item; + }) + }); + }} + /> + ), + additional: (record, index) => + InstanceRenderManager({ + rome: ( + + {() => { + const billLine = getFieldValue(["billlines", record.name]); + const jobLine = lineData.find((line) => line.id === billLine?.joblineid); + + if (!billEdit && billLine && jobLine && billLine?.actual_price !== jobLine?.act_price) { + return ( + + + + + {t("joblines.fields.create_ppc")} + + ); + } else { + return null; + } + }} + + ) + //Do not need to set for promanager as it will default to Rome. + }) + }, + { + title: t("billlines.fields.actual_cost"), + dataIndex: "actual_cost", + editable: true, + width: "8rem", + + formItemProps: (field) => { + return { + key: `${field.index}actual_cost`, + name: [field.name, "actual_cost"], + label: t("billlines.fields.actual_cost"), + rules: [ + { + required: true + //message: t("general.validation.required"), + } + ] + }; + }, + formInput: (record, index) => ( + + {() => { + const line = getFieldsValue(["billlines"]).billlines[index]; + if (!!!line) return null; + let lineDiscount = 1 - line.actual_cost / line.actual_price; + if (isNaN(lineDiscount)) lineDiscount = 0; + return ( + + 0.005 + ? lineDiscount > discount + ? "orange" + : "red" + : "green" + }} + /> + + ); + }} + + } + /> + ) + // additional: (record, index) => ( + // + // {() => { + // const line = getFieldsValue(["billlines"]).billlines[index]; + // if (!!!line) return null; + // const lineDiscount = ( + // 1 - + // Math.round((line.actual_cost / line.actual_price) * 100) / 100 + // ).toPrecision(2); + + // return ( + // + // + // + // ); + // }} + // + // ), + }, + { + title: t("billlines.fields.cost_center"), + dataIndex: "cost_center", + editable: true, + + formItemProps: (field) => { + return { + key: `${field.index}cost_center`, + name: [field.name, "cost_center"], + label: t("billlines.fields.cost_center"), + valuePropName: "value", + rules: [ + { + required: true + //message: t("general.validation.required"), + } + ] + }; + }, + formInput: (record, index) => ( + + {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber + ? CiecaSelect(true, false) + : responsibilityCenters.costs.map((item) => {item.name})} + + ) + }, + ...(billEdit + ? [] + : [ + { + title: t("billlines.fields.location"), + dataIndex: "location", + editable: true, + label: t("billlines.fields.location"), + formItemProps: (field) => { + return { + key: `${field.index}location`, + name: [field.name, "location"] + }; + }, + formInput: (record, index) => ( + + {bodyshop.md_parts_locations.map((loc, idx) => ( + + {loc} + + ))} + + ) + } + ]), + { + title: t("billlines.labels.deductedfromlbr"), + dataIndex: "deductedfromlbr", + editable: true, + formItemProps: (field) => { + return { + valuePropName: "checked", + key: `${field.index}deductedfromlbr`, + name: [field.name, "deductedfromlbr"] + }; + }, + formInput: (record, index) => , + additional: (record, index) => ( + + {() => { + const price = getFieldValue(["billlines", record.name, "actual_price"]); + + const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]); + + const billline = getFieldValue(["billlines", record.name]); + + const jobline = lineData.find((line) => line.id === billline?.joblineid); + + const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team); + + if (getFieldValue(["billlines", record.name, "deductedfromlbr"])) + return ( + + {Enhanced_Payroll.treatment === "on" ? ( + + {t("joblines.fields.assigned_team", { + name: employeeTeamName?.name + })} + {`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`} + + ) : null} + + + + {t("joblines.fields.lbr_types.LAA")} + {t("joblines.fields.lbr_types.LAB")} + {t("joblines.fields.lbr_types.LAD")} + {t("joblines.fields.lbr_types.LAE")} + {t("joblines.fields.lbr_types.LAF")} + {t("joblines.fields.lbr_types.LAG")} + {t("joblines.fields.lbr_types.LAM")} + {t("joblines.fields.lbr_types.LAR")} + {t("joblines.fields.lbr_types.LAS")} + {t("joblines.fields.lbr_types.LAU")} + {t("joblines.fields.lbr_types.LA1")} + {t("joblines.fields.lbr_types.LA2")} + {t("joblines.fields.lbr_types.LA3")} + {t("joblines.fields.lbr_types.LA4")} + + + {Enhanced_Payroll.treatment === "on" ? ( + + + + ) : ( + + + + )} + + {price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`} + + ); + return <>>; + }} + + ) + }, + + ...InstanceRenderManager({ + rome: [], + promanager: [], + imex: [ + { + title: t("billlines.fields.federal_tax_applicable"), + dataIndex: "applicable_taxes.federal", + editable: true, + + formItemProps: (field) => { + return { + key: `${field.index}fedtax`, + valuePropName: "checked", + initialValue: InstanceRenderManager({ + imex: true, + rome: false, + promanager: false + }), + name: [field.name, "applicable_taxes", "federal"] + }; + }, + formInput: (record, index) => + } + ] + }), + + { + title: t("billlines.fields.state_tax_applicable"), + dataIndex: "applicable_taxes.state", + editable: true, + + formItemProps: (field) => { + return { + key: `${field.index}statetax`, + valuePropName: "checked", + name: [field.name, "applicable_taxes", "state"] + }; + }, + formInput: (record, index) => + }, + + ...InstanceRenderManager({ + rome: [], + promanager: [], + imex: [ + { + title: t("billlines.fields.local_tax_applicable"), + dataIndex: "applicable_taxes.local", + editable: true, + + formItemProps: (field) => { + return { + key: `${field.index}localtax`, + valuePropName: "checked", + name: [field.name, "applicable_taxes", "local"] + }; + }, + formInput: (record, index) => + } + ] + }), + { + title: t("general.labels.actions"), + + dataIndex: "actions", + render: (text, record) => ( + + {() => ( + + 0} + onClick={() => remove(record.name)} + > + + + {Simple_Inventory.treatment === "on" && ( + + )} + + )} + + ) + } + ]; + }; + + const mergedColumns = (remove) => + columns(remove).map((col) => { + if (!col.editable) return col; + return { + ...col, + onCell: (record) => ({ + record, + formItemProps: col.formItemProps, + formInput: col.formInput, + additional: col.additional, + dataIndex: col.dataIndex, + title: col.title + }) + }; }); - const columns = (remove) => { - return [ - { - title: t("billlines.fields.jobline"), - dataIndex: "joblineid", - editable: true, - width: "20rem", - formItemProps: (field) => { - return { - key: `${field.index}joblinename`, - name: [field.name, "joblineid"], - label: t("billlines.fields.jobline"), - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ], - }; - }, - wrapper: (props) => ( - - prev.is_credit_memo !== cur.is_credit_memo - } - > - {() => { - return props.children; - }} - - ), - formInput: (record, index) => ( - { - setFieldsValue({ - billlines: getFieldsValue([ - "billlines", - ]).billlines.map((item, idx) => { - if (idx === index) { - return { - ...item, - line_desc: opt.line_desc, - quantity: opt.part_qty || 1, - actual_price: opt.cost, - original_actual_price: opt.cost, - cost_center: opt.part_type - ? bodyshop.pbs_serialnumber || - bodyshop.cdk_dealerid - ? opt.part_type !== "PAE" - ? opt.part_type - : null - : responsibilityCenters.defaults && - (responsibilityCenters - .defaults.costs[ - opt.part_type - ] || - null) - : null, - }; - } - return item; - }), - }); - }} - /> - ), - }, - { - title: t("billlines.fields.line_desc"), - dataIndex: "line_desc", - editable: true, - - formItemProps: (field) => { - return { - key: `${field.index}line_desc`, - name: [field.name, "line_desc"], - label: t("billlines.fields.line_desc"), - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ], - }; - }, - formInput: (record, index) => , - }, - { - title: t("billlines.fields.quantity"), - dataIndex: "quantity", - editable: true, - width: "4rem", - formItemProps: (field) => { - return { - key: `${field.index}quantity`, - name: [field.name, "quantity"], - label: t("billlines.fields.quantity"), - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ({ getFieldValue }) => ({ - validator(rule, value) { - if ( - value && - getFieldValue("billlines")[ - field.fieldKey - ]?.inventories?.length > value - ) { - return Promise.reject( - t( - "bills.validation.inventoryquantity", - { - number: getFieldValue( - "billlines" - )[field.fieldKey] - ?.inventories?.length, - } - ) - ); - } - return Promise.resolve(); - }, - }), - ], - }; - }, - formInput: (record, index) => ( - - ), - }, - { - title: t("billlines.fields.actual_price"), - dataIndex: "actual_price", - width: "8rem", - editable: true, - formItemProps: (field) => { - return { - key: `${field.index}actual_price`, - name: [field.name, "actual_price"], - label: t("billlines.fields.actual_price"), - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ], - }; - }, - formInput: (record, index) => ( - { - setFieldsValue({ - billlines: getFieldsValue( - "billlines" - ).billlines.map((item, idx) => { - if (idx === index) { - return { - ...item, - actual_cost: !!item.actual_cost - ? item.actual_cost - : Math.round( - (parseFloat( - e.target.value - ) * - (1 - discount) + - Number.EPSILON) * - 100 - ) / 100, - }; - } - return item; - }), - }); - }} - /> - ), - additional: (record, index) => - InstanceRenderManager({ - rome: ( - - {() => { - const billLine = getFieldValue([ - "billlines", - record.name, - ]); - const jobLine = lineData.find( - (line) => - line.id === billLine?.joblineid - ); - - if ( - !billEdit && - billLine && - jobLine && - billLine?.actual_price !== - jobLine?.act_price - ) { - return ( - - - - - {t( - "joblines.fields.create_ppc" - )} - - ); - } else { - return null; - } - }} - - ), - //Do not need to set for promanager as it will default to Rome. - }), - }, - { - title: t("billlines.fields.actual_cost"), - dataIndex: "actual_cost", - editable: true, - width: "8rem", - - formItemProps: (field) => { - return { - key: `${field.index}actual_cost`, - name: [field.name, "actual_cost"], - label: t("billlines.fields.actual_cost"), - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ], - }; - }, - formInput: (record, index) => ( - - {() => { - const line = getFieldsValue(["billlines"]) - .billlines[index]; - if (!!!line) return null; - let lineDiscount = - 1 - - line.actual_cost / line.actual_price; - if (isNaN(lineDiscount)) lineDiscount = 0; - return ( - - 0.005 - ? lineDiscount > - discount - ? "orange" - : "red" - : "green", - }} - /> - - ); - }} - - } - /> - ), - // additional: (record, index) => ( - // - // {() => { - // const line = getFieldsValue(["billlines"]).billlines[index]; - // if (!!!line) return null; - // const lineDiscount = ( - // 1 - - // Math.round((line.actual_cost / line.actual_price) * 100) / 100 - // ).toPrecision(2); - - // return ( - // - // - // - // ); - // }} - // - // ), - }, - { - title: t("billlines.fields.cost_center"), - dataIndex: "cost_center", - editable: true, - - formItemProps: (field) => { - return { - key: `${field.index}cost_center`, - name: [field.name, "cost_center"], - label: t("billlines.fields.cost_center"), - valuePropName: "value", - rules: [ - { - required: true, - //message: t("general.validation.required"), - }, - ], - }; - }, - formInput: (record, index) => ( - - {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber - ? CiecaSelect(true, false) - : responsibilityCenters.costs.map((item) => ( - - {item.name} - - ))} - - ), - }, - ...(billEdit - ? [] - : [ - { - title: t("billlines.fields.location"), - dataIndex: "location", - editable: true, - label: t("billlines.fields.location"), - formItemProps: (field) => { - return { - key: `${field.index}location`, - name: [field.name, "location"], - }; - }, - formInput: (record, index) => ( - - {bodyshop.md_parts_locations.map( - (loc, idx) => ( - - {loc} - - ) - )} - - ), - }, - ]), - { - title: t("billlines.labels.deductedfromlbr"), - dataIndex: "deductedfromlbr", - editable: true, - formItemProps: (field) => { - return { - valuePropName: "checked", - key: `${field.index}deductedfromlbr`, - name: [field.name, "deductedfromlbr"], - }; - }, - formInput: (record, index) => , - additional: (record, index) => ( - - {() => { - const price = getFieldValue([ - "billlines", - record.name, - "actual_price", - ]); - - const adjustmentRate = getFieldValue([ - "billlines", - record.name, - "lbr_adjustment", - "rate", - ]); - - const billline = getFieldValue([ - "billlines", - record.name, - ]); - - const jobline = lineData.find( - (line) => line.id === billline?.joblineid - ); - - const employeeTeamName = - bodyshop.employee_teams.find( - (team) => team.id === jobline?.assigned_team - ); - - if ( - getFieldValue([ - "billlines", - record.name, - "deductedfromlbr", - ]) - ) - return ( - - {Enhanced_Payroll.treatment === "on" ? ( - - {t( - "joblines.fields.assigned_team", - { - name: employeeTeamName?.name, - } - )} - {`${ - jobline.mod_lb_hrs - } units/${t( - `joblines.fields.lbr_types.${jobline.mod_lbr_ty}` - )}`} - - ) : null} - - - - - {t( - "joblines.fields.lbr_types.LAA" - )} - - - {t( - "joblines.fields.lbr_types.LAB" - )} - - - {t( - "joblines.fields.lbr_types.LAD" - )} - - - {t( - "joblines.fields.lbr_types.LAE" - )} - - - {t( - "joblines.fields.lbr_types.LAF" - )} - - - {t( - "joblines.fields.lbr_types.LAG" - )} - - - {t( - "joblines.fields.lbr_types.LAM" - )} - - - {t( - "joblines.fields.lbr_types.LAR" - )} - - - {t( - "joblines.fields.lbr_types.LAS" - )} - - - {t( - "joblines.fields.lbr_types.LAU" - )} - - - {t( - "joblines.fields.lbr_types.LA1" - )} - - - {t( - "joblines.fields.lbr_types.LA2" - )} - - - {t( - "joblines.fields.lbr_types.LA3" - )} - - - {t( - "joblines.fields.lbr_types.LA4" - )} - - - - {Enhanced_Payroll.treatment === "on" ? ( - - - - ) : ( - - - - )} - - - {price && - adjustmentRate && - `${( - price / adjustmentRate - ).toFixed(1)} hrs`} - - - ); - return <>>; - }} - - ), - }, - - ...InstanceRenderManager({ - imex: [ - { - title: t("billlines.fields.federal_tax_applicable"), - dataIndex: "applicable_taxes.federal", - editable: true, - - formItemProps: (field) => { - return { - key: `${field.index}fedtax`, - valuePropName: 'checked', - initialValue: InstanceRenderManager({ - imex: true, - rome: false, - promanager: false, - }), - name: [field.name, 'applicable_taxes', 'federal'], - }; - }, - formInput: (record, index) => ( - - ), - }, - ], - }), - - { - title: t("billlines.fields.state_tax_applicable"), - dataIndex: "applicable_taxes.state", - editable: true, - - formItemProps: (field) => { - return { - key: `${field.index}statetax`, - valuePropName: "checked", - name: [field.name, "applicable_taxes", "state"], - }; - }, - formInput: (record, index) => , - }, - - ...InstanceRenderManager({ - imex: [ - { - title: t("billlines.fields.local_tax_applicable"), - dataIndex: "applicable_taxes.local", - editable: true, - - formItemProps: (field) => { - return { - key: `${field.index}localtax`, - valuePropName: "checked", - name: [field.name, "applicable_taxes", "local"], - }; - }, - formInput: (record, index) => ( - - ), - }, - ], - }), - { - title: t("general.labels.actions"), - - dataIndex: "actions", - render: (text, record) => ( - - {() => ( - - 0 - } - onClick={() => remove(record.name)} - > - - - {Simple_Inventory.treatment === "on" && ( - - )} - - )} - - ), - }, - ]; - }; - - const mergedColumns = (remove) => - columns(remove).map((col) => { - if (!col.editable) return col; - return { - ...col, - onCell: (record) => ({ - record, - formItemProps: col.formItemProps, - formInput: col.formInput, - additional: col.additional, - dataIndex: col.dataIndex, - title: col.title, - }), - }; - }); - - return ( - { - if (!billlines || billlines.length < 1) { - return Promise.reject( - new Error(t("billlines.validation.atleastone")) - ); - } - }, - }, - ]} - > - {(fields, { add, remove, move }) => { - return ( - <> - - - { - add(); - }} - style={{ width: "100%" }} - > - {t("billlines.actions.newline")} - - - > - ); - }} - - ); + return ( + { + if (!billlines || billlines.length < 1) { + return Promise.reject(new Error(t("billlines.validation.atleastone"))); + } + } + } + ]} + > + {(fields, { add, remove, move }) => { + return ( + <> + + + { + add(); + }} + style={{ width: "100%" }} + > + {t("billlines.actions.newline")} + + + > + ); + }} + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillEnterModalLinesComponent); +export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent); const EditableCell = ({ - dataIndex, - title, - inputType, - record, - index, - children, - formInput, - formItemProps, - additional, - wrapper, - ...restProps + dataIndex, + title, + inputType, + record, + index, + children, + formInput, + formItemProps, + additional, + wrapper, + ...restProps }) => { - if (additional) - return ( - - - - {(formInput && formInput(record, record.name)) || - children} - - {additional && additional(record, record.name)} - - - ); - if (wrapper) - return ( - - - - {(formInput && formInput(record, record.name)) || - children} - - - - ); + if (additional) return ( - - - {(formInput && formInput(record, record.name)) || children} - - + + + + {(formInput && formInput(record, record.name)) || children} + + {additional && additional(record, record.name)} + + ); + if (wrapper) + return ( + + + + {(formInput && formInput(record, record.name)) || children} + + + + ); + return ( + + + {(formInput && formInput(record, record.name)) || children} + + + ); }; diff --git a/client/src/components/bill-form/bill-form.totals.utility.js b/client/src/components/bill-form/bill-form.totals.utility.js index eb5c67517..cec5c9152 100644 --- a/client/src/components/bill-form/bill-form.totals.utility.js +++ b/client/src/components/bill-form/bill-form.totals.utility.js @@ -1,47 +1,42 @@ import Dinero from "dinero.js"; export const CalculateBillTotal = (invoice) => { - const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} = - invoice; + const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice; - //TODO Determine why this recalculates so many times. - let subtotal = Dinero({amount: 0}); - let federalTax = Dinero({amount: 0}); - let stateTax = Dinero({amount: 0}); - let localTax = Dinero({amount: 0}); + //TODO Determine why this recalculates so many times. + let subtotal = Dinero({ amount: 0 }); + let federalTax = Dinero({ amount: 0 }); + let stateTax = Dinero({ amount: 0 }); + let localTax = Dinero({ amount: 0 }); - if (!!!billlines) return null; + if (!!!billlines) return null; - billlines.forEach((i) => { - if (!!i) { - const itemTotal = Dinero({ - amount: Math.round((i.actual_cost || 0) * 100), - }).multiply(i.quantity || 1); + billlines.forEach((i) => { + if (!!i) { + const itemTotal = Dinero({ + amount: Math.round((i.actual_cost || 0) * 100) + }).multiply(i.quantity || 1); - subtotal = subtotal.add(itemTotal); - if (i.applicable_taxes?.federal) { - federalTax = federalTax.add( - itemTotal.percentage(federal_tax_rate || 0) - ); - } - if (i.applicable_taxes?.state) - stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0)); - if (i.applicable_taxes?.local) - localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0)); - } - }); + subtotal = subtotal.add(itemTotal); + if (i.applicable_taxes?.federal) { + federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0)); + } + if (i.applicable_taxes?.state) stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0)); + if (i.applicable_taxes?.local) localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0)); + } + }); - const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)}); - const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax); - const discrepancy = enteredTotal.subtract(invoiceTotal); + const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) }); + const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax); + const discrepancy = enteredTotal.subtract(invoiceTotal); - return { - subtotal, - federalTax, - stateTax, - localTax, - enteredTotal, - invoiceTotal, - discrepancy, - }; + return { + subtotal, + federalTax, + stateTax, + localTax, + enteredTotal, + invoiceTotal, + discrepancy + }; }; diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx index a9cab74b9..7cbdd27b9 100644 --- a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx +++ b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx @@ -1,173 +1,153 @@ -import {Checkbox, Form, Skeleton, Typography} from "antd"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; +import { Checkbox, Form, Skeleton, Typography } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import "./bill-inventory-table.styles.scss"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {selectBillEnterModal} from "../../redux/modals/modals.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - billEnterModal: selectBillEnterModal, + bodyshop: selectBodyshop, + billEnterModal: selectBillEnterModal }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable); -export function BillInventoryTable({ - billEnterModal, - bodyshop, - form, - billEdit, - inventoryLoading, - inventoryData, - }) { - const {t} = useTranslation(); +export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) { + const { t } = useTranslation(); - useEffect(() => { - if (inventoryData && inventoryData.inventory) { - form.setFieldsValue({ - inventory: billEnterModal.context.consumeinventoryid - ? inventoryData.inventory.map((i) => { - if (i.id === billEnterModal.context.consumeinventoryid) - i.consumefrominventory = true; - return i; - }) - : inventoryData.inventory, - }); + useEffect(() => { + if (inventoryData && inventoryData.inventory) { + form.setFieldsValue({ + inventory: billEnterModal.context.consumeinventoryid + ? inventoryData.inventory.map((i) => { + if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true; + return i; + }) + : inventoryData.inventory + }); + } + }, [inventoryData, form, billEnterModal.context.consumeinventoryid]); + + return ( + prev.vendorid !== cur.vendorid} noStyle> + {() => { + const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid; + + if (!is_inhouse || billEdit) { + return null; } - }, [inventoryData, form, billEnterModal.context.consumeinventoryid]); - return ( - prev.vendorid !== cur.vendorid} - noStyle - > - {() => { - const is_inhouse = - form.getFieldValue("vendorid") === bodyshop.inhousevendorid; + if (inventoryLoading) return ; - if (!is_inhouse || billEdit) { - return null; - } + return ( + + {(fields, { add, remove, move }) => { + return ( + <> + {t("inventory.labels.inventory")} + + + + {t("billlines.fields.line_desc")} + {t("vendors.fields.name")} + {t("billlines.fields.quantity")} + {t("billlines.fields.actual_price")} + {t("billlines.fields.actual_cost")} + {t("inventory.fields.comment")} + {t("inventory.actions.consumefrominventory")} + + + + {fields.map((field, index) => ( + + + + + + - if (inventoryLoading) return ; + + + + + + + + + + + + + + + + + + + + + + + + + - return ( - - {(fields, {add, remove, move}) => { - return ( - <> - - {t("inventory.labels.inventory")} - - - - - {t("billlines.fields.line_desc")} - {t("vendors.fields.name")} - {t("billlines.fields.quantity")} - {t("billlines.fields.actual_price")} - {t("billlines.fields.actual_cost")} - {t("inventory.fields.comment")} - {t("inventory.actions.consumefrominventory")} - - - - {fields.map((field, index) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - - - > - ); - }} - - ); + + + + + + + ))} + + + > + ); }} - - ); + + ); + }} + + ); } diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss index 9a47de1dc..173714f41 100644 --- a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss +++ b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss @@ -16,4 +16,4 @@ tr:hover { background-color: #f5f5f5; } -} \ No newline at end of file +} diff --git a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx index 75616849a..187cf29bf 100644 --- a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx +++ b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx @@ -1,98 +1,73 @@ -import {Select} from "antd"; -import React, {forwardRef} from "react"; -import {useTranslation} from "react-i18next"; -import InstanceRenderMgr from '../../utils/instanceRenderMgr'; +import { Select } from "antd"; +import React, { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; +import InstanceRenderMgr from "../../utils/instanceRenderMgr"; //To be used as a form element only. -const {Option} = Select; -const BillLineSearchSelect = ( - {options, disabled, allowRemoved, ...restProps}, - ref -) => { - const {t} = useTranslation(); +const { Option } = Select; +const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => { + const { t } = useTranslation(); - return ( - { - return ( - (option.line_desc && - option.line_desc - .toLowerCase() - .includes(inputValue.toLowerCase())) || - (option.oem_partno && - option.oem_partno - .toLowerCase() - .includes(inputValue.toLowerCase())) || - (option.alt_partno && - option.alt_partno - .toLowerCase() - .includes(inputValue.toLowerCase())) || - (option.act_price && - option.act_price.toString().startsWith(inputValue.toString())) - ); - }} - notFoundContent={"Removed."} - {...restProps} - > - - {t("billlines.labels.other")} - - {options - ? options.map((item) => ( - + return ( + { + return ( + (option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) || + (option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) || + (option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) || + (option.act_price && option.act_price.toString().startsWith(inputValue.toString())) + ); + }} + notFoundContent={"Removed."} + {...restProps} + > + + {t("billlines.labels.other")} + + {options + ? options.map((item) => ( + {`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ - item.oem_partno ? ` - ${item.oem_partno}` : "" + item.oem_partno ? ` - ${item.oem_partno}` : "" }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()} - { - InstanceRenderMgr - ( - { - rome: item.act_price === 0 && item.mod_lb_hrs > 0 && ( - - {`${item.mod_lb_hrs} units`} - - ) + {InstanceRenderMgr({ + rome: item.act_price === 0 && item.mod_lb_hrs > 0 && ( + {`${item.mod_lb_hrs} units`} + ) + })} - } - ) - - - } - - - {item.act_price - ? `$${item.act_price && item.act_price.toFixed(2)}` - : ``} + + {item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``} - - )) - : null} - - ); + + )) + : null} + + ); }; export default forwardRef(BillLineSearchSelect); diff --git a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx index a014bf775..5a5720135 100644 --- a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx +++ b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx @@ -1,97 +1,89 @@ -import {gql, useMutation} from "@apollo/client"; -import {Button, notification} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import { gql, useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component"; -import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; +import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - authLevel: selectAuthLevel, - currentUser: selectCurrentUser, + bodyshop: selectBodyshop, + authLevel: selectAuthLevel, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillMarkExportedButton); +export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton); -export function BillMarkExportedButton({ - currentUser, - bodyshop, - authLevel, - bill, - }) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); - const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); +export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); - const [updateBill] = useMutation(gql` - mutation UPDATE_BILL($billId: uuid!) { - update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) { - returning { - id - exported - exported_at - } - } + const [updateBill] = useMutation(gql` + mutation UPDATE_BILL($billId: uuid!) { + update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) { + returning { + id + exported + exported_at } - `); + } + } + `); - const handleUpdate = async () => { - setLoading(true); - const result = await updateBill({ - variables: {billId: bill.id}, - }); - - await insertExportLog({ - variables: { - logs: [ - { - bodyshopid: bodyshop.id, - billid: bill.id, - successful: true, - message: JSON.stringify([t("general.labels.markedexported")]), - useremail: currentUser.email, - }, - ], - }, - }); - - if (!result.errors) { - notification["success"]({ - message: t("bills.successes.markexported"), - }); - } else { - notification["error"]({ - message: t("bills.errors.saving", { - error: JSON.stringify(result.errors), - }), - }); - } - setLoading(false); - //Get the owner details, populate it all back into the job. - }; - - const hasAccess = HasRbacAccess({ - bodyshop, - authLevel, - action: "bills:reexport", + const handleUpdate = async () => { + setLoading(true); + const result = await updateBill({ + variables: { billId: bill.id } }); - if (hasAccess) - return ( - - {t("bills.labels.markexported")} - - ); + await insertExportLog({ + variables: { + logs: [ + { + bodyshopid: bodyshop.id, + billid: bill.id, + successful: true, + message: JSON.stringify([t("general.labels.markedexported")]), + useremail: currentUser.email + } + ] + } + }); - return <>>; + if (!result.errors) { + notification["success"]({ + message: t("bills.successes.markexported") + }); + } else { + notification["error"]({ + message: t("bills.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } + setLoading(false); + //Get the owner details, populate it all back into the job. + }; + + const hasAccess = HasRbacAccess({ + bodyshop, + authLevel, + action: "bills:reexport" + }); + + if (hasAccess) + return ( + + {t("bills.labels.markexported")} + + ); + + return <>>; } diff --git a/client/src/components/bill-print-button/bill-print-button.component.jsx b/client/src/components/bill-print-button/bill-print-button.component.jsx index dd3bac20e..f63b9a13f 100644 --- a/client/src/components/bill-print-button/bill-print-button.component.jsx +++ b/client/src/components/bill-print-button/bill-print-button.component.jsx @@ -1,38 +1,38 @@ -import {Button, Space} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { Button, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; -export default function BillPrintButton({billid}) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); - const Templates = TemplateList("job_special"); +export default function BillPrintButton({ billid }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const Templates = TemplateList("job_special"); - const submitHandler = async () => { - setLoading(true); - try { - await GenerateDocument( - { - name: Templates.parts_invoice_label_single.key, - variables: { - id: billid, - }, - }, - {}, - "p" - ); - } catch (e) { - console.warn("Warning: Error generating a document."); - } - setLoading(false); - }; + const submitHandler = async () => { + setLoading(true); + try { + await GenerateDocument( + { + name: Templates.parts_invoice_label_single.key, + variables: { + id: billid + } + }, + {}, + "p" + ); + } catch (e) { + console.warn("Warning: Error generating a document."); + } + setLoading(false); + }; - return ( - - - {t("bills.labels.printlabels")} - - - ); + return ( + + + {t("bills.labels.printlabels")} + + + ); } diff --git a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx index 3f5ab32b8..81da224d7 100644 --- a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx +++ b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx @@ -1,79 +1,72 @@ -import {gql, useMutation} from "@apollo/client"; -import {Button, notification} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import { gql, useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors"; -import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - authLevel: selectAuthLevel, + bodyshop: selectBodyshop, + authLevel: selectAuthLevel }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillMarkForReexportButton); +export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton); -export function BillMarkForReexportButton({bodyshop, authLevel, bill}) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); +export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); - const [updateBill] = useMutation(gql` - mutation UPDATE_BILL($billId: uuid!) { - update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) { - returning { - id - exported - exported_at - } - } + const [updateBill] = useMutation(gql` + mutation UPDATE_BILL($billId: uuid!) { + update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) { + returning { + id + exported + exported_at } - `); + } + } + `); - const handleUpdate = async () => { - setLoading(true); - const result = await updateBill({ - variables: {billId: bill.id}, - }); - - if (!result.errors) { - notification["success"]({ - message: t("bills.successes.reexport"), - }); - } else { - notification["error"]({ - message: t("bills.errors.saving", { - error: JSON.stringify(result.errors), - }), - }); - } - setLoading(false); - //Get the owner details, populate it all back into the job. - }; - - const hasAccess = HasRbacAccess({ - bodyshop, - authLevel, - action: "bills:reexport", + const handleUpdate = async () => { + setLoading(true); + const result = await updateBill({ + variables: { billId: bill.id } }); - if (hasAccess) - return ( - - {t("bills.labels.markforreexport")} - - ); + if (!result.errors) { + notification["success"]({ + message: t("bills.successes.reexport") + }); + } else { + notification["error"]({ + message: t("bills.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } + setLoading(false); + //Get the owner details, populate it all back into the job. + }; - return <>>; + const hasAccess = HasRbacAccess({ + bodyshop, + authLevel, + action: "bills:reexport" + }); + + if (hasAccess) + return ( + + {t("bills.labels.markforreexport")} + + ); + + return <>>; } diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx index 5b35bd196..63d322b79 100644 --- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -1,151 +1,136 @@ -import {FileAddFilled} from "@ant-design/icons"; -import {useMutation} from "@apollo/client"; -import {Button, notification, Tooltip} from "antd"; -import {t} from "i18next"; +import { FileAddFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, notification, Tooltip } from "antd"; +import { t } from "i18next"; import dayjs from "./../../utils/day"; -import React, {useState} from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; import queryString from "query-string"; -import {useLocation} from "react-router-dom"; +import { useLocation } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(BilllineAddInventory); +export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory); -export function BilllineAddInventory({ - currentUser, - bodyshop, - billline, - disabled, - jobid, - }) { - const [loading, setLoading] = useState(false); - const {billid} = queryString.parse(useLocation().search); - const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT); +export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) { + const [loading, setLoading] = useState(false); + const { billid } = queryString.parse(useLocation().search); + const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT); - const addToInventory = async () => { - setLoading(true); + const addToInventory = async () => { + setLoading(true); - //Check to make sure there are no existing items already in the inventory. + //Check to make sure there are no existing items already in the inventory. - const cm = { - vendorid: bodyshop.inhousevendorid, - invoice_number: "ih", - jobid: jobid, - isinhouse: true, - is_credit_memo: true, - date: dayjs().format("YYYY-MM-DD"), - federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, - state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, - local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, - total: 0, - billlines: [ - { - actual_price: billline.actual_price, - actual_cost: billline.actual_cost, - quantity: billline.quantity, - line_desc: billline.line_desc, - cost_center: billline.cost_center, - deductedfromlbr: billline.deductedfromlbr, - applicable_taxes: { - local: billline.applicable_taxes.local, - state: billline.applicable_taxes.state, - federal: billline.applicable_taxes.federal, - }, - }, - ], - }; - - cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100; - - const insertResult = await insertInventoryLine({ - variables: { - joblineId: - billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line. - //Unfortunately, we can't send null as the GQL syntax validation fails. - joblineStatus: bodyshop.md_order_statuses.default_returned, - inv: { - shopid: bodyshop.id, - billlineid: billline.id, - actual_price: billline.actual_price, - actual_cost: billline.actual_cost, - quantity: billline.quantity, - line_desc: billline.line_desc, - }, - cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert. - pol: { - returnfrombill: billid, - vendorid: bodyshop.inhousevendorid, - deliver_by: dayjs().format("YYYY-MM-DD"), - parts_order_lines: { - data: [ - { - line_desc: billline.line_desc, - - act_price: billline.actual_price, - cost: billline.actual_cost, - quantity: billline.quantity, - job_line_id: - billline.joblineid === "noline" ? null : billline.joblineid, - part_type: billline.jobline && billline.jobline.part_type, - cm_received: true, - }, - ], - }, - order_date: "2022-06-01", - orderedby: currentUser.email, - jobid: jobid, - user_email: currentUser.email, - return: true, - status: "Ordered", - }, - }, - refetchQueries: ["QUERY_BILL_BY_PK"], - }); - - if (!insertResult.errors) { - notification.open({ - type: "success", - message: t("inventory.successes.inserted"), - }); - } else { - notification.open({ - type: "error", - message: t("inventory.errors.inserting", { - error: JSON.stringify(insertResult.errors), - }), - }); + const cm = { + vendorid: bodyshop.inhousevendorid, + invoice_number: "ih", + jobid: jobid, + isinhouse: true, + is_credit_memo: true, + date: dayjs().format("YYYY-MM-DD"), + federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, + state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, + local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, + total: 0, + billlines: [ + { + actual_price: billline.actual_price, + actual_cost: billline.actual_cost, + quantity: billline.quantity, + line_desc: billline.line_desc, + cost_center: billline.cost_center, + deductedfromlbr: billline.deductedfromlbr, + applicable_taxes: { + local: billline.applicable_taxes.local, + state: billline.applicable_taxes.state, + federal: billline.applicable_taxes.federal + } } - - setLoading(false); + ] }; - return ( - - = billline.quantity - } - onClick={addToInventory} - > - - {billline?.inventories?.length > 0 && ( - ({billline?.inventories?.length} in inv) - )} - - - ); + cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100; + + const insertResult = await insertInventoryLine({ + variables: { + joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line. + //Unfortunately, we can't send null as the GQL syntax validation fails. + joblineStatus: bodyshop.md_order_statuses.default_returned, + inv: { + shopid: bodyshop.id, + billlineid: billline.id, + actual_price: billline.actual_price, + actual_cost: billline.actual_cost, + quantity: billline.quantity, + line_desc: billline.line_desc + }, + cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert. + pol: { + returnfrombill: billid, + vendorid: bodyshop.inhousevendorid, + deliver_by: dayjs().format("YYYY-MM-DD"), + parts_order_lines: { + data: [ + { + line_desc: billline.line_desc, + + act_price: billline.actual_price, + cost: billline.actual_cost, + quantity: billline.quantity, + job_line_id: billline.joblineid === "noline" ? null : billline.joblineid, + part_type: billline.jobline && billline.jobline.part_type, + cm_received: true + } + ] + }, + order_date: "2022-06-01", + orderedby: currentUser.email, + jobid: jobid, + user_email: currentUser.email, + return: true, + status: "Ordered" + } + }, + refetchQueries: ["QUERY_BILL_BY_PK"] + }); + + if (!insertResult.errors) { + notification.open({ + type: "success", + message: t("inventory.successes.inserted") + }); + } else { + notification.open({ + type: "error", + message: t("inventory.errors.inserting", { + error: JSON.stringify(insertResult.errors) + }) + }); + } + + setLoading(false); + }; + + return ( + + = billline.quantity} + onClick={addToInventory} + > + + {billline?.inventories?.length > 0 && ({billline?.inventories?.length} in inv)} + + + ); } diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx index a03dea379..5db868406 100644 --- a/client/src/components/bills-list-table/bills-list-table.component.jsx +++ b/client/src/components/bills-list-table/bills-list-table.component.jsx @@ -1,236 +1,209 @@ -import {EditFilled, SyncOutlined} from "@ant-design/icons"; -import {Button, Card, Checkbox, Input, Space, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectJobReadOnly} from "../../redux/application/application.selectors"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { EditFilled, SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Checkbox, Input, Space, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {DateFormatter} from "../../utils/DateFormatter"; -import {alphaSort, dateSort} from "../../utils/sorters"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { DateFormatter } from "../../utils/DateFormatter"; +import { alphaSort, dateSort } from "../../utils/sorters"; +import { TemplateList } from "../../utils/TemplateConstants"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; const mapStateToProps = createStructuredSelector({ - jobRO: selectJobReadOnly, - bodyshop: selectBodyshop, + jobRO: selectJobReadOnly, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setPartsOrderContext: (context) => - dispatch(setModalContext({context: context, modal: "partsOrder"})), - setBillEnterContext: (context) => - dispatch(setModalContext({context: context, modal: "billEnter"})), - setReconciliationContext: (context) => - dispatch(setModalContext({context: context, modal: "reconciliation"})), + setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), + setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), + setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" })) }); export function BillsListTableComponent({ - bodyshop, - jobRO, - job, - billsQuery, - handleOnRowClick, - setPartsOrderContext, - setBillEnterContext, - setReconciliationContext, - }) { - const {t} = useTranslation(); + bodyshop, + jobRO, + job, + billsQuery, + handleOnRowClick, + setPartsOrderContext, + setBillEnterContext, + setReconciliationContext +}) { + const { t } = useTranslation(); - const [state, setState] = useState({ - sortedInfo: {}, - }); - // const search = queryString.parse(useLocation().search); - // const selectedBill = search.billid; - const [searchText, setSearchText] = useState(""); + const [state, setState] = useState({ + sortedInfo: {} + }); + // const search = queryString.parse(useLocation().search); + // const selectedBill = search.billid; + const [searchText, setSearchText] = useState(""); - const Templates = TemplateList("bill"); - const bills = billsQuery.data ? billsQuery.data.bills : []; - const { refetch } = billsQuery; - const recordActions = (record, showView = false) => ( + const Templates = TemplateList("bill"); + const bills = billsQuery.data ? billsQuery.data.bills : []; + const { refetch } = billsQuery; + const recordActions = (record, showView = false) => ( + + {showView && ( + handleOnRowClick(record)}> + + + )} + + + + {record.isinhouse && ( + + )} + + ); + const columns = [ + { + title: t("bills.fields.vendorname"), + dataIndex: "vendorname", + key: "vendorname", + sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), + sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, + render: (text, record) => {record.vendor.name} + }, + { + title: t("bills.fields.invoice_number"), + dataIndex: "invoice_number", + key: "invoice_number", + sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), + sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order + }, + { + title: t("bills.fields.date"), + dataIndex: "date", + key: "date", + sorter: (a, b) => dateSort(a.date, b.date), + sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order, + render: (text, record) => {record.date} + }, + { + title: t("bills.fields.total"), + dataIndex: "total", + key: "total", + sorter: (a, b) => a.total - b.total, + sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order, + render: (text, record) => {record.total} + }, + { + title: t("bills.fields.is_credit_memo"), + dataIndex: "is_credit_memo", + key: "is_credit_memo", + sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, + sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order, + render: (text, record) => + }, + { + title: t("bills.fields.exported"), + dataIndex: "exported", + key: "exported", + sorter: (a, b) => a.exported - b.exported, + sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order, + render: (text, record) => + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => recordActions(record, true) + } + ]; + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + + const filteredBills = bills + ? searchText === "" + ? bills + : bills.filter( + (b) => + (b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) || + (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) || + (b.total || "").toString().toLowerCase().includes(searchText.toLowerCase()) + ) + : []; + + return ( + - {showView && ( - handleOnRowClick(record)}> - - - )} - - - - {record.isinhouse && ( - - )} - - ); - const columns = [ - { - title: t("bills.fields.vendorname"), - dataIndex: "vendorname", - key: "vendorname", - sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), - sortOrder: - state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, - render: (text, record) => {record.vendor.name}, - }, - { - title: t("bills.fields.invoice_number"), - dataIndex: "invoice_number", - key: "invoice_number", - sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), - sortOrder: - state.sortedInfo.columnKey === "invoice_number" && - state.sortedInfo.order, - }, - { - title: t("bills.fields.date"), - dataIndex: "date", - key: "date", - sorter: (a, b) => dateSort(a.date, b.date), - sortOrder: - state.sortedInfo.columnKey === "date" && state.sortedInfo.order, - render: (text, record) => {record.date}, - }, - { - title: t("bills.fields.total"), - dataIndex: "total", - key: "total", - sorter: (a, b) => a.total - b.total, - sortOrder: - state.sortedInfo.columnKey === "total" && state.sortedInfo.order, - render: (text, record) => ( - {record.total} - ), - }, - { - title: t("bills.fields.is_credit_memo"), - dataIndex: "is_credit_memo", - key: "is_credit_memo", - sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, - sortOrder: - state.sortedInfo.columnKey === "is_credit_memo" && - state.sortedInfo.order, - render: (text, record) => , - }, - { - title: t("bills.fields.exported"), - dataIndex: "exported", - key: "exported", - sorter: (a, b) => a.exported - b.exported, - sortOrder: - state.sortedInfo.columnKey === "exported" && state.sortedInfo.order, - render: (text, record) => , - }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", - render: (text, record) => recordActions(record, true), - }, - ]; - - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; - - const filteredBills = bills - ? searchText === "" - ? bills - : bills.filter( - (b) => - (b.invoice_number || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (b.vendor.name || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (b.total || "") - .toString() - .toLowerCase() - .includes(searchText.toLowerCase()) - ) - : []; - - return ( - - refetch()}> - - - {job && job.converted ? ( - <> - { - setBillEnterContext({ - actions: {refetch: billsQuery.refetch}, - context: { - job, - }, - }); - }} - > - {t("jobs.actions.postbills")} - - { - setReconciliationContext({ - actions: {refetch: billsQuery.refetch}, - context: { - job, - bills: (billsQuery.data && billsQuery.data.bills) || [], - }, - }); - }} - > - {t("jobs.actions.reconcile")} - - > - ) : null} - - { - e.preventDefault(); - setSearchText(e.target.value); - }} - /> - - } - > - refetch()}> + + + {job && job.converted ? ( + <> + { + setBillEnterContext({ + actions: { refetch: billsQuery.refetch }, + context: { + job + } + }); }} - columns={columns} - rowKey="id" - dataSource={filteredBills} - onChange={handleTableChange} - /> - - ); + > + {t("jobs.actions.postbills")} + + { + setReconciliationContext({ + actions: { refetch: billsQuery.refetch }, + context: { + job, + bills: (billsQuery.data && billsQuery.data.bills) || [] + } + }); + }} + > + {t("jobs.actions.reconcile")} + + > + ) : null} + + { + e.preventDefault(); + setSearchText(e.target.value); + }} + /> + + } + > + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillsListTableComponent); +export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent); diff --git a/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx b/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx index e6cf58cb9..433f18960 100644 --- a/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx +++ b/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx @@ -1,121 +1,112 @@ -import React, {useState} from "react"; -import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries"; -import {useQuery} from "@apollo/client"; +import React, { useState } from "react"; +import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; +import { useQuery } from "@apollo/client"; import queryString from "query-string"; -import {useLocation, useNavigate} from "react-router-dom"; -import {Input, Table} from "antd"; -import {useTranslation} from "react-i18next"; -import {alphaSort} from "../../utils/sorters"; +import { useLocation, useNavigate } from "react-router-dom"; +import { Input, Table } from "antd"; +import { useTranslation } from "react-i18next"; +import { alphaSort } from "../../utils/sorters"; import AlertComponent from "../alert/alert.component"; export default function BillsVendorsList() { - const search = queryString.parse(useLocation().search); - const history = useNavigate(); + const search = queryString.parse(useLocation().search); + const history = useNavigate(); - const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - const {t} = useTranslation(); + const { t } = useTranslation(); - const [state, setState] = useState({ - sortedInfo: {}, - search: "", - }); + const [state, setState] = useState({ + sortedInfo: {}, + search: "" + }); - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const columns = [ - { - title: t("vendors.fields.name"), - dataIndex: "name", - key: "name", - sorter: (a, b) => alphaSort(a.name, b.name), - sortOrder: - state.sortedInfo.columnKey === "name" && state.sortedInfo.order, + const columns = [ + { + title: t("vendors.fields.name"), + dataIndex: "name", + key: "name", + sorter: (a, b) => alphaSort(a.name, b.name), + sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order + }, + { + title: t("vendors.fields.cost_center"), + dataIndex: "cost_center", + key: "cost_center", + sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), + sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order + }, + { + title: t("vendors.fields.city"), + dataIndex: "city", + key: "city" + } + ]; + + const handleOnRowClick = (record) => { + if (record) { + delete search.billid; + if (record.id) { + search.vendorid = record.id; + history.push({ search: queryString.stringify(search) }); + } + } else { + delete search.vendorid; + history.push({ search: queryString.stringify(search) }); + } + }; + + const handleSearch = (e) => { + setState({ ...state, search: e.target.value }); + }; + + if (error) return ; + + const dataSource = state.search + ? data.vendors.filter( + (v) => + (v.name || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) || + (v.city || "").toLowerCase().includes(state.search.toLowerCase()) + ) + : (data && data.vendors) || []; + + return ( + { + return ( + + + + ); + }} + dataSource={dataSource} + pagination={{ position: "top" }} + columns={columns} + rowKey="id" + onChange={handleTableChange} + rowSelection={{ + onSelect: (record) => { + handleOnRowClick(record); }, - { - title: t("vendors.fields.cost_center"), - dataIndex: "cost_center", - key: "cost_center", - sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), - sortOrder: - state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, - }, - { - title: t("vendors.fields.city"), - dataIndex: "city", - key: "city", - }, - ]; - - const handleOnRowClick = (record) => { - if (record) { - delete search.billid; - if (record.id) { - search.vendorid = record.id; - history.push({search: queryString.stringify(search)}); - } - } else { - delete search.vendorid; - history.push({search: queryString.stringify(search)}); - } - }; - - const handleSearch = (e) => { - setState({...state, search: e.target.value}); - }; - - if (error) return ; - - const dataSource = state.search - ? data.vendors.filter( - (v) => - (v.name || "").toLowerCase().includes(state.search.toLowerCase()) || - (v.cost_center || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (v.city || "").toLowerCase().includes(state.search.toLowerCase()) - ) - : (data && data.vendors) || []; - - return ( - { - return ( - - - - ); - }} - dataSource={dataSource} - pagination={{position: "top"}} - columns={columns} - rowKey="id" - onChange={handleTableChange} - rowSelection={{ - onSelect: (record) => { - handleOnRowClick(record); - }, - selectedRowKeys: [search.vendorid], - type: "radio", - }} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); - }, // click row - }; - }} - /> - ); + selectedRowKeys: [search.vendorid], + type: "radio" + }} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); + } // click row + }; + }} + /> + ); } diff --git a/client/src/components/breadcrumbs/breadcrumbs.component.jsx b/client/src/components/breadcrumbs/breadcrumbs.component.jsx index 360eb480d..93a9d0192 100644 --- a/client/src/components/breadcrumbs/breadcrumbs.component.jsx +++ b/client/src/components/breadcrumbs/breadcrumbs.component.jsx @@ -1,64 +1,63 @@ -import {HomeFilled} from "@ant-design/icons"; -import {Breadcrumb, Col, Row} from "antd"; +import { HomeFilled } from "@ant-design/icons"; +import { Breadcrumb, Col, Row } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {Link} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {selectBreadcrumbs} from "../../redux/application/application.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { selectBreadcrumbs } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import GlobalSearch from "../global-search/global-search.component"; import GlobalSearchOs from "../global-search/global-search-os.component"; import "./breadcrumbs.styles.scss"; -import {useSplitTreatments} from "@splitsoftware/splitio-react"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; const mapStateToProps = createStructuredSelector({ - breadcrumbs: selectBreadcrumbs, - bodyshop: selectBodyshop, + breadcrumbs: selectBreadcrumbs, + bodyshop: selectBodyshop }); -export function BreadCrumbs({breadcrumbs, bodyshop}) { - - const {treatments: {OpenSearch}} = useSplitTreatments({ - attributes: {}, - names: ["OpenSearch"], - splitKey: bodyshop && bodyshop.imexshopid, - }); - // TODO - Client Update - Technically key is not doing anything here - return ( - - - - {" "} - {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || - ""} - - ), - }, - ...breadcrumbs.map((item) => - item.link - ? { - key: item.label, - title: {item.label}, - } - : { - key: item.label, - title: item.label, - } - ), - ]} - /> - - - {OpenSearch.treatment === "on" ? : } - - - ); +export function BreadCrumbs({ breadcrumbs, bodyshop }) { + const { + treatments: { OpenSearch } + } = useSplitTreatments({ + attributes: {}, + names: ["OpenSearch"], + splitKey: bodyshop && bodyshop.imexshopid + }); + // TODO - Client Update - Technically key is not doing anything here + return ( + + + + {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""} + + ) + }, + ...breadcrumbs.map((item) => + item.link + ? { + key: item.label, + title: {item.label} + } + : { + key: item.label, + title: item.label + } + ) + ]} + /> + + + {OpenSearch.treatment === "on" ? : } + + + ); } export default connect(mapStateToProps, null)(BreadCrumbs); diff --git a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx index 04a49ca36..04c49e2db 100644 --- a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx +++ b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx @@ -1,99 +1,87 @@ -import {Button, Form, Modal} from "antd"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { Button, Form, Modal } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component"; const mapStateToProps = createStructuredSelector({ - caBcEtfTableModal: selectCaBcEtfTableConvert, + caBcEtfTableModal: selectCaBcEtfTableConvert }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => - dispatch(toggleModalVisible("ca_bc_eftTableConvert")), + toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert")) }); -export function ContractsFindModalContainer({ - caBcEtfTableModal, - toggleModalVisible, - }) { - const {t} = useTranslation(); +export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) { + const { t } = useTranslation(); - const {open} = caBcEtfTableModal; - const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); - const EtfTemplate = TemplateList("special").ca_bc_etf_table; + const { open } = caBcEtfTableModal; + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const EtfTemplate = TemplateList("special").ca_bc_etf_table; - const handleFinish = async (values) => { - logImEXEvent("ca_bc_etf_table_parse"); - setLoading(true); - const claimNumbers = []; - values.table.split("\n").forEach((row, idx, arr) => { - const {1: claim, 2: shortclaim, 4: amount} = row.split("\t"); - if (!claim || !shortclaim) return; - const trimmedShortClaim = shortclaim.trim(); - // const trimmedClaim = claim.trim(); - if (amount.slice(-1) === "-") { - } + const handleFinish = async (values) => { + logImEXEvent("ca_bc_etf_table_parse"); + setLoading(true); + const claimNumbers = []; + values.table.split("\n").forEach((row, idx, arr) => { + const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t"); + if (!claim || !shortclaim) return; + const trimmedShortClaim = shortclaim.trim(); + // const trimmedClaim = claim.trim(); + if (amount.slice(-1) === "-") { + } - claimNumbers.push({ - claim: trimmedShortClaim, - amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount, - }); - }); + claimNumbers.push({ + claim: trimmedShortClaim, + amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount + }); + }); - await GenerateDocument( - { - name: EtfTemplate.key, - variables: { - claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`, - claimdata: claimNumbers, - }, - }, - {}, - values.sendby === "email" ? "e" : "p" - ); - setLoading(false); - }; - - useEffect(() => { - if (open) { - form.resetFields(); + await GenerateDocument( + { + name: EtfTemplate.key, + variables: { + claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`, + claimdata: claimNumbers } - }, [open, form]); - - return ( - toggleModalVisible()} - onOk={() => toggleModalVisible()} - destroyOnClose - forceRender - > - - - form.submit()} type="primary" loading={loading}> - {t("general.labels.search")} - - - + }, + {}, + values.sendby === "email" ? "e" : "p" ); + setLoading(false); + }; + + useEffect(() => { + if (open) { + form.resetFields(); + } + }, [open, form]); + + return ( + toggleModalVisible()} + onOk={() => toggleModalVisible()} + destroyOnClose + forceRender + > + + + form.submit()} type="primary" loading={loading}> + {t("general.labels.search")} + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ContractsFindModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer); diff --git a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx index 67659eaa6..e9224ae6b 100644 --- a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx +++ b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx @@ -1,42 +1,38 @@ -import {Form, Input, Radio} from "antd"; +import { Form, Input, Radio } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); export default connect(mapStateToProps, null)(PartsReceiveModalComponent); -export function PartsReceiveModalComponent({bodyshop, form}) { - const {t} = useTranslation(); +export function PartsReceiveModalComponent({ bodyshop, form }) { + const { t } = useTranslation(); - return ( - - - - - - - {t("general.labels.email")} - {t("general.labels.print")} - - - - ); + return ( + + + + + + + {t("general.labels.email")} + {t("general.labels.print")} + + + + ); } diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx index 1cb2f3385..cbdd26770 100644 --- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx +++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx @@ -1,50 +1,45 @@ -import React, {useState} from "react"; -import {Button, Form, InputNumber, Popover} from "antd"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {useTranslation} from "react-i18next"; -import {CalculatorFilled} from "@ant-design/icons"; +import React, { useState } from "react"; +import { Button, Form, InputNumber, Popover } from "antd"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { useTranslation } from "react-i18next"; +import { CalculatorFilled } from "@ant-design/icons"; -export default function CABCpvrtCalculator({disabled, form}) { - const [visibility, setVisibility] = useState(false); +export default function CABCpvrtCalculator({ disabled, form }) { + const [visibility, setVisibility] = useState(false); - const {t} = useTranslation(); + const { t } = useTranslation(); - const handleFinish = async (values) => { - logImEXEvent("job_ca_bc_pvrt_calculate"); - form.setFieldsValue({ - ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2), - }); - form.setFields([{name: "ca_bc_pvrt", touched: true}]); - setVisibility(false); - }; + const handleFinish = async (values) => { + logImEXEvent("job_ca_bc_pvrt_calculate"); + form.setFieldsValue({ + ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2) + }); + form.setFields([{ name: "ca_bc_pvrt", touched: true }]); + setVisibility(false); + }; - const popContent = ( - - - - - - - - - - {t("general.actions.calculate")} - - setVisibility(false)}>Close - - - ); + const popContent = ( + + + + + + + + + + {t("general.actions.calculate")} + + setVisibility(false)}>Close + + + ); - return ( - - setVisibility(true)}> - - - - ); + return ( + + setVisibility(true)}> + + + + ); } diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx index 581fe7334..bec81dd2e 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx @@ -1,357 +1,328 @@ -import {DeleteFilled} from "@ant-design/icons"; -import {useLazyQuery, useMutation} from "@apollo/client"; -import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd"; +import { DeleteFilled } from "@ant-design/icons"; +import { useLazyQuery, useMutation } from "@apollo/client"; +import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd"; import axios from "axios"; import dayjs from "../../utils/day"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries"; -import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries"; -import {insertAuditTrail} from "../../redux/application/application.actions"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectCardPayment} from "../../redux/modals/modals.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries"; +import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectCardPayment } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; const mapStateToProps = createStructuredSelector({ - cardPaymentModal: selectCardPayment, - bodyshop: selectBodyshop, + cardPaymentModal: selectCardPayment, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - insertAuditTrail: ({jobid, operation, type}) => - dispatch(insertAuditTrail({jobid, operation, type})), - toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })), + toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) }); -const CardPaymentModalComponent = ({ - bodyshop, - cardPaymentModal, - toggleModalVisible, - insertAuditTrail, - }) => { - const {context} = cardPaymentModal; +const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => { + const { context } = cardPaymentModal; - const [form] = Form.useForm(); + const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); - const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); - const {t} = useTranslation(); + const [loading, setLoading] = useState(false); + const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); + const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); + const { t } = useTranslation(); - const [, {data, refetch, queryLoading}] = useLazyQuery( - QUERY_RO_AND_OWNER_BY_JOB_PKS, - { - variables: {jobids: [context.jobid]}, - skip: true, + const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, { + variables: { jobids: [context.jobid] }, + skip: true + }); + + console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data); + //Initialize the intellipay window. + const SetIntellipayCallbackFunctions = () => { + console.log("*** Set IntelliPay callback functions."); + window.intellipay.runOnClose(() => { + //window.intellipay.initialize(); + }); + + window.intellipay.runOnApproval(async function (response) { + console.warn("*** Running On Approval Script ***"); + form.setFieldValue("paymentResponse", response); + form.submit(); + }); + + window.intellipay.runOnNonApproval(async function (response) { + // Mutate unsuccessful payment + + const { payments } = form.getFieldsValue(); + + await insertPaymentResponse({ + variables: { + paymentResponse: payments.map((payment) => ({ + amount: payment.amount, + bodyshopid: bodyshop.id, + jobid: payment.jobid, + declinereason: response.declinereason, + ext_paymentid: response.paymentid.toString(), + successful: false, + response + })) } - ); + }); - console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data); - //Initialize the intellipay window. - const SetIntellipayCallbackFunctions = () => { - console.log("*** Set IntelliPay callback functions."); - window.intellipay.runOnClose(() => { - //window.intellipay.initialize(); - }); + payments.forEach((payment) => + insertAuditTrail({ + jobid: payment.jobid, + operation: AuditTrailMapping.failedpayment(), + type: "failedpayment" + }) + ); + }); + }; - window.intellipay.runOnApproval(async function (response) { - console.warn("*** Running On Approval Script ***"); - form.setFieldValue("paymentResponse", response); - form.submit(); - }); + const handleFinish = async (values) => { + try { + await insertPayment({ + variables: { + paymentInput: values.payments.map((payment) => ({ + amount: payment.amount, + transactionid: (values.paymentResponse.paymentid || "").toString(), + payer: t("payments.labels.customer"), + type: values.paymentResponse.cardbrand, + jobid: payment.jobid, + date: dayjs(Date.now()), + payment_responses: { + data: [ + { + amount: payment.amount, + bodyshopid: bodyshop.id, - window.intellipay.runOnNonApproval(async function (response) { - // Mutate unsuccessful payment - - const {payments} = form.getFieldsValue(); - - await insertPaymentResponse({ - variables: { - paymentResponse: payments.map((payment) => ({ - amount: payment.amount, - bodyshopid: bodyshop.id, - jobid: payment.jobid, - declinereason: response.declinereason, - ext_paymentid: response.paymentid.toString(), - successful: false, - response, - })), - }, - }); - - payments.forEach((payment) => - insertAuditTrail({ - jobid: payment.jobid, - operation: AuditTrailMapping.failedpayment(), - type: "failedpayment",}) - ); - }); - }; - - const handleFinish = async (values) => { - try { - await insertPayment({ - variables: { - paymentInput: values.payments.map((payment) => ({ - amount: payment.amount, - transactionid: (values.paymentResponse.paymentid || "").toString(), - payer: t("payments.labels.customer"), - type: values.paymentResponse.cardbrand, - jobid: payment.jobid, - date: dayjs(Date.now()), - payment_responses: { - data: [ - { - amount: payment.amount, - bodyshopid: bodyshop.id, - - jobid: payment.jobid, - declinereason: values.paymentResponse.declinereason, - ext_paymentid: values.paymentResponse.paymentid.toString(), - successful: true, - response: values.paymentResponse, - }, - ], - }, - })), - }, - refetchQueries: ["GET_JOB_BY_PK"], - }); - toggleModalVisible(); - } catch (error) { - console.error(error); - notification.open({ - type: "error", - message: t("payments.errors.inserting", {error: error.message}), - }); - } finally { - setLoading(false); - } - }; - - const handleIntelliPayCharge = async () => { - setLoading(true); - - //Validate - try { - await form.validateFields(); - } catch (error) { - setLoading(false); - return; - } - - try { - const response = await axios.post("/intellipay/lightbox_credentials", { - bodyshop, - refresh: !!window.intellipay, - }); - - if (window.intellipay) { - // eslint-disable-next-line no-eval - eval(response.data); - SetIntellipayCallbackFunctions(); - window.intellipay.autoOpen(); - } else { - var rg = document.createRange(); - let node = rg.createContextualFragment(response.data); - document.documentElement.appendChild(node); - SetIntellipayCallbackFunctions(); - window.intellipay.isAutoOpen = true; - window.intellipay.initialize(); + jobid: payment.jobid, + declinereason: values.paymentResponse.declinereason, + ext_paymentid: values.paymentResponse.paymentid.toString(), + successful: true, + response: values.paymentResponse + } + ] } - } catch (error) { - notification.open({ - type: "error", - message: t("job_payments.notifications.error.openingip"), - }); - setLoading(false); - } - }; + })) + }, + refetchQueries: ["GET_JOB_BY_PK"] + }); + toggleModalVisible(); + } catch (error) { + console.error(error); + notification.open({ + type: "error", + message: t("payments.errors.inserting", { error: error.message }) + }); + } finally { + setLoading(false); + } + }; - return ( - - - - - {(fields, {add, remove, move}) => { - return ( - - {fields.map((field, index) => ( - - - - - - - - - - - - - - { - remove(field.name); - }} - /> - - - - ))} - - { - add(); - }} - style={{width: "100%"}} - > - {t("general.actions.add")} - - - - ); - }} - + const handleIntelliPayCharge = async () => { + setLoading(true); - - prevValues.payments?.map((p) => p?.jobid).join() !== - curValues.payments?.map((p) => p?.jobid).join() - } + //Validate + try { + await form.validateFields(); + } catch (error) { + setLoading(false); + return; + } + + try { + const response = await axios.post("/intellipay/lightbox_credentials", { + bodyshop, + refresh: !!window.intellipay + }); + + if (window.intellipay) { + // eslint-disable-next-line no-eval + eval(response.data); + SetIntellipayCallbackFunctions(); + window.intellipay.autoOpen(); + } else { + var rg = document.createRange(); + let node = rg.createContextualFragment(response.data); + document.documentElement.appendChild(node); + SetIntellipayCallbackFunctions(); + window.intellipay.isAutoOpen = true; + window.intellipay.initialize(); + } + } catch (error) { + notification.open({ + type: "error", + message: t("job_payments.notifications.error.openingip") + }); + setLoading(false); + } + }; + + return ( + + + + + {(fields, { add, remove, move }) => { + return ( + + {fields.map((field, index) => ( + + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + ))} + + { + add(); + }} + style={{ width: "100%" }} > - {() => { - console.log("Updating the owner info section."); - //If all of the job ids have been fileld in, then query and update the IP field. - const {payments} = form.getFieldsValue(); - if ( - payments?.length > 0 && - payments?.filter((p) => p?.jobid).length === payments?.length - ) { - console.log("**Calling refetch."); - refetch({jobids: payments.map((p) => p.jobid)}); - } - console.log( - "Acc info", - data, - payments && data && data.jobs.length > 0 - ? data.jobs.map((j) => j.ro_number).join(", ") - : null - ); - return ( - <> - 0 - ? data.jobs.map((j) => j.ro_number).join(", ") - : null - } - /> - 0 - ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea - : null - } - /> - > - ); - }} - - - prevValues.payments?.map((p) => p?.amount).join() !== - curValues.payments?.map((p) => p?.amount).join() - } - > - {() => { - const {payments} = form.getFieldsValue(); - const totalAmountToCharge = payments?.reduce((acc, val) => { - return acc + (val?.amount || 0); - }, 0); + {t("general.actions.add")} + + + + ); + }} + - return ( - - - - 0)} - onClick={handleIntelliPayCharge} - > - {t("job_payments.buttons.proceedtopayment")} - - - ); - }} - + + prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join() + } + > + {() => { + console.log("Updating the owner info section."); + //If all of the job ids have been fileld in, then query and update the IP field. + const { payments } = form.getFieldsValue(); + if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) { + console.log("**Calling refetch."); + refetch({ jobids: payments.map((p) => p.jobid) }); + } + console.log( + "Acc info", + data, + payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null + ); + return ( + <> + 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null + } + /> + 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null + } + /> + > + ); + }} + + + prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join() + } + > + {() => { + const { payments } = form.getFieldsValue(); + const totalAmountToCharge = payments?.reduce((acc, val) => { + return acc + (val?.amount || 0); + }, 0); - {/* Lightbox payment response when it is completed */} - - - - - - - ); + return ( + + + + 0)} + onClick={handleIntelliPayCharge} + > + {t("job_payments.buttons.proceedtopayment")} + + + ); + }} + + + {/* Lightbox payment response when it is completed */} + + + + + + + ); }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(CardPaymentModalComponent); +export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent); diff --git a/client/src/components/card-payment-modal/card-payment-modal.container..jsx b/client/src/components/card-payment-modal/card-payment-modal.container..jsx index 4984ddce5..2f56615bd 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.container..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.container..jsx @@ -1,57 +1,50 @@ -import {Button, Modal} from "antd"; +import { Button, Modal } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectCardPayment} from "../../redux/modals/modals.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectCardPayment } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CardPaymentModalComponent from "./card-payment-modal.component."; const mapStateToProps = createStructuredSelector({ - cardPaymentModal: selectCardPayment, - bodyshop: selectBodyshop, + cardPaymentModal: selectCardPayment, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), + toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) }); -function CardPaymentModalContainer({ - cardPaymentModal, - toggleModalVisible, - bodyshop, - }) { - const {open} = cardPaymentModal; - const {t} = useTranslation(); +function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) { + const { open } = cardPaymentModal; + const { t } = useTranslation(); - const handleCancel = () => { - toggleModalVisible(); - }; + const handleCancel = () => { + toggleModalVisible(); + }; - const handleOK = () => { - toggleModalVisible(); - }; + const handleOK = () => { + toggleModalVisible(); + }; - return ( - - {t("job_payments.buttons.goback")} - , - ]} - width="80%" - destroyOnClose - > - - - ); + return ( + + {t("job_payments.buttons.goback")} + + ]} + width="80%" + destroyOnClose + > + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(CardPaymentModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer); diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index d82115e72..0541cd167 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -1,100 +1,97 @@ -import {useApolloClient} from "@apollo/client"; -import {getToken, onMessage} from "@firebase/messaging"; -import {Button, notification, Space} from "antd"; +import { useApolloClient } from "@apollo/client"; +import { getToken, onMessage } from "@firebase/messaging"; +import { Button, notification, Space } from "antd"; import axios from "axios"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {messaging, requestForToken} from "../../firebase/firebase.utils"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { messaging, requestForToken } from "../../firebase/firebase.utils"; import FcmHandler from "../../utils/fcm-handler"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; -export function ChatAffixContainer({bodyshop, chatVisible}) { - const {t} = useTranslation(); - const client = useApolloClient(); - - useEffect(() => { - if (!bodyshop || !bodyshop.messagingservicesid) return; +export function ChatAffixContainer({ bodyshop, chatVisible }) { + const { t } = useTranslation(); + const client = useApolloClient(); - async function SubscribeToTopic() { - try { - const r = await axios.post("/notifications/subscribe", { - fcm_tokens: await getToken(messaging, { - vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY, - }), - type: "messaging", - imexshopid: bodyshop.imexshopid, - }); - console.log("FCM Topic Subscription", r.data); - } catch (error) { - console.log( - "Error attempting to subscribe to messaging topic: ", - error - ); - notification.open({ - key: 'fcm', - type: "warning", - message: t("general.errors.fcm"), - btn: ( - - { - await requestForToken(); - SubscribeToTopic(); - }} - > - {t("general.actions.tryagain")} - - { - const win = window.open( - "https://help.imex.online/en/article/enabling-notifications-o978xi/", - "_blank" - ); - win.focus(); - }} - > - {t("general.labels.help")} - - - ), - }); - } - } + useEffect(() => { + if (!bodyshop || !bodyshop.messagingservicesid) return; - SubscribeToTopic(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [bodyshop]); + async function SubscribeToTopic() { + try { + const r = await axios.post("/notifications/subscribe", { + fcm_tokens: await getToken(messaging, { + vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY + }), + type: "messaging", + imexshopid: bodyshop.imexshopid + }); + console.log("FCM Topic Subscription", r.data); + } catch (error) { + console.log("Error attempting to subscribe to messaging topic: ", error); + notification.open({ + key: "fcm", + type: "warning", + message: t("general.errors.fcm"), + btn: ( + + { + await requestForToken(); + SubscribeToTopic(); + }} + > + {t("general.actions.tryagain")} + + { + const win = window.open( + "https://help.imex.online/en/article/enabling-notifications-o978xi/", + "_blank" + ); + win.focus(); + }} + > + {t("general.labels.help")} + + + ) + }); + } + } - useEffect(() => { - function handleMessage(payload) { - FcmHandler({ - client, - payload: (payload && payload.data && payload.data.data) || payload.data, - }); - } + SubscribeToTopic(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bodyshop]); - let stopMessageListener, channel; - try { - stopMessageListener = onMessage(messaging, handleMessage); - channel = new BroadcastChannel("imex-sw-messages"); - channel.addEventListener("message", handleMessage); - } catch (error) { - console.log("Unable to set event listeners."); - } - return () => { - stopMessageListener && stopMessageListener(); - channel && channel.removeEventListener("message", handleMessage); - }; - }, [client]); + useEffect(() => { + function handleMessage(payload) { + FcmHandler({ + client, + payload: (payload && payload.data && payload.data.data) || payload.data + }); + } - if (!bodyshop || !bodyshop.messagingservicesid) return <>>; + let stopMessageListener, channel; + try { + stopMessageListener = onMessage(messaging, handleMessage); + channel = new BroadcastChannel("imex-sw-messages"); + channel.addEventListener("message", handleMessage); + } catch (error) { + console.log("Unable to set event listeners."); + } + return () => { + stopMessageListener && stopMessageListener(); + channel && channel.removeEventListener("message", handleMessage); + }; + }, [client]); - return ( - - {bodyshop && bodyshop.messagingservicesid ? : null} - - ); + if (!bodyshop || !bodyshop.messagingservicesid) return <>>; + + return ( + + {bodyshop && bodyshop.messagingservicesid ? : null} + + ); } export default ChatAffixContainer; diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx index d7233d2cf..755e8f514 100644 --- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx +++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx @@ -1,29 +1,27 @@ -import {useMutation} from "@apollo/client"; -import {Button} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {TOGGLE_CONVERSATION_ARCHIVE} from "../../graphql/conversations.queries"; +import { useMutation } from "@apollo/client"; +import { Button } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries"; -export default function ChatArchiveButton({conversation}) { - const [loading, setLoading] = useState(false); - const {t} = useTranslation(); - const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); - const handleToggleArchive = async () => { - setLoading(true); +export default function ChatArchiveButton({ conversation }) { + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); + const handleToggleArchive = async () => { + setLoading(true); - await updateConversation({ - variables: {id: conversation.id, archived: !conversation.archived}, - refetchQueries: ["CONVERSATION_LIST_QUERY"], - }); + await updateConversation({ + variables: { id: conversation.id, archived: !conversation.archived }, + refetchQueries: ["CONVERSATION_LIST_QUERY"] + }); - setLoading(false); - }; + setLoading(false); + }; - return ( - - {conversation.archived - ? t("messaging.labels.unarchive") - : t("messaging.labels.archive")} - - ); + return ( + + {conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")} + + ); } diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index d498949cf..a49c3b4b4 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,122 +1,101 @@ -import {Badge, Card, List, Space, Tag} from "antd"; +import { Badge, Card, List, Space, Tag } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized"; -import {createStructuredSelector} from "reselect"; -import {setSelectedConversation} from "../../redux/messaging/messaging.actions"; -import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors"; -import {TimeAgoFormatter} from "../../utils/DateFormatter"; +import { connect } from "react-redux"; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized"; +import { createStructuredSelector } from "reselect"; +import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; +import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; +import { TimeAgoFormatter } from "../../utils/DateFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter"; -import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component"; +import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import _ from "lodash"; import "./chat-conversation-list.styles.scss"; const mapStateToProps = createStructuredSelector({ - selectedConversation: selectSelectedConversation, + selectedConversation: selectSelectedConversation }); const mapDispatchToProps = (dispatch) => ({ - setSelectedConversation: (conversationId) => - dispatch(setSelectedConversation(conversationId)), + setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId)) }); function ChatConversationListComponent({ - conversationList, - selectedConversation, - setSelectedConversation, - loadMoreConversations, - }) { - const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 60, - }); + conversationList, + selectedConversation, + setSelectedConversation, + loadMoreConversations +}) { + const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 60 + }); - const rowRenderer = ({index, key, style, parent}) => { - const item = conversationList[index]; - const cardContentRight = - {item.updated_at}; - const cardContentLeft = item.job_conversations.length > 0 - ? item.job_conversations.map((j, idx) => ( - {j.job.ro_number} - )) - : null; + const rowRenderer = ({ index, key, style, parent }) => { + const item = conversationList[index]; + const cardContentRight = {item.updated_at}; + const cardContentLeft = + item.job_conversations.length > 0 + ? item.job_conversations.map((j, idx) => {j.job.ro_number}) + : null; - const names = <>{_.uniq(item.job_conversations.map((j, idx) => - OwnerNameDisplayFunction(j.job) - ))}> + const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}>; - const cardTitle = <> - {item.label && {item.label}} - {item.job_conversations.length > 0 ? ( - - {names} - - ) : ( - - {item.phone_num} - - )} - > - const cardExtra = + const cardTitle = ( + <> + {item.label && {item.label}} + {item.job_conversations.length > 0 ? ( + {names} + ) : ( + + {item.phone_num} + + )} + > + ); + const cardExtra = ; - const getCardStyle = () => - item.id === selectedConversation - ? {backgroundColor: 'rgba(128, 128, 128, 0.2)'} - : {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'}; - - return ( - - setSelectedConversation(item.id)} - style={style} - className={`chat-list-item - ${ - item.id === selectedConversation - ? "chat-list-selected-conversation" - : null - }`} - > - - - {cardContentLeft} - - {cardContentRight} - - - - ); - }; + const getCardStyle = () => + item.id === selectedConversation + ? { backgroundColor: "rgba(128, 128, 128, 0.2)" } + : { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" }; return ( - - - {({height, width}) => ( - { - if (scrollTop + clientHeight === scrollHeight) { - loadMoreConversations(); - } - }} - /> - )} - - + + setSelectedConversation(item.id)} + style={style} + className={`chat-list-item + ${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`} + > + + {cardContentLeft} + {cardContentRight} + + + ); + }; + + return ( + + + {({ height, width }) => ( + { + if (scrollTop + clientHeight === scrollHeight) { + loadMoreConversations(); + } + }} + /> + )} + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ChatConversationListComponent); +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationListComponent); diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index bdeb99ae2..d851e6c92 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -1,56 +1,56 @@ -import {useMutation} from "@apollo/client"; -import {Tag} from "antd"; +import { useMutation } from "@apollo/client"; +import { Tag } from "antd"; import React from "react"; -import {Link} from "react-router-dom"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {REMOVE_CONVERSATION_TAG} from "../../graphql/job-conversations.queries"; +import { Link } from "react-router-dom"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -export default function ChatConversationTitleTags({jobConversations}) { - const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); +export default function ChatConversationTitleTags({ jobConversations }) { + const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); - const handleRemoveTag = (jobId) => { - const convId = jobConversations[0].conversationid; - if (!!convId) { - removeJobConversation({ - variables: { - conversationId: convId, - jobId: jobId, - }, - update(cache) { - cache.modify({ - id: cache.identify({id: convId, __typename: "conversations"}), - fields: { - job_conversations(ex) { - return ex.filter((e) => e.jobid !== jobId); - }, - }, - }); - }, - }); - logImEXEvent("messaging_remove_job_tag", { - conversationId: convId, - jobId: jobId, - }); + const handleRemoveTag = (jobId) => { + const convId = jobConversations[0].conversationid; + if (!!convId) { + removeJobConversation({ + variables: { + conversationId: convId, + jobId: jobId + }, + update(cache) { + cache.modify({ + id: cache.identify({ id: convId, __typename: "conversations" }), + fields: { + job_conversations(ex) { + return ex.filter((e) => e.jobid !== jobId); + } + } + }); } - }; + }); + logImEXEvent("messaging_remove_job_tag", { + conversationId: convId, + jobId: jobId + }); + } + }; - return ( - - {jobConversations.map((item) => ( - handleRemoveTag(item.job.id)} - > - - {`${item.job.ro_number || "?"} | `} - - - - ))} - - ); + return ( + + {jobConversations.map((item) => ( + handleRemoveTag(item.job.id)} + > + + {`${item.job.ro_number || "?"} | `} + + + + ))} + + ); } diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index bdc1a35a3..41cf0b441 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -1,4 +1,4 @@ -import {Space} from "antd"; +import { Space } from "antd"; import React from "react"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component"; @@ -7,21 +7,15 @@ import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; -export default function ChatConversationTitle({conversation}) { - return ( - - - {conversation && conversation.phone_num} - - - - - - - - ); +export default function ChatConversationTitle({ conversation }) { + return ( + + {conversation && conversation.phone_num} + + + + + + + ); } diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index 7e11bef10..d4a9695c0 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -6,26 +6,21 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import "./chat-conversation.styles.scss"; -export default function ChatConversationComponent({ - subState, - conversation, - messages, - handleMarkConversationAsRead, - }) { - const [loading, error] = subState; +export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) { + const [loading, error] = subState; - if (loading) return ; - if (error) return ; + if (loading) return ; + if (error) return ; - return ( - - - - - - ); + return ( + + + + + + ); } diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index d989fe514..2b91d2320 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,87 +1,81 @@ -import {useMutation, useQuery, useSubscription} from "@apollo/client"; -import React, {useState} from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries"; -import {MARK_MESSAGES_AS_READ_BY_CONVERSATION} from "../../graphql/messages.queries"; -import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors"; +import { useMutation, useQuery, useSubscription } from "@apollo/client"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; +import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries"; +import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import ChatConversationComponent from "./chat-conversation.component"; import axios from "axios"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - selectedConversation: selectSelectedConversation, - bodyshop: selectBodyshop, + selectedConversation: selectSelectedConversation, + bodyshop: selectBodyshop }); export default connect(mapStateToProps, null)(ChatConversationContainer); -export function ChatConversationContainer({bodyshop, selectedConversation}) { - const { - loading: convoLoading, - error: convoError, - data: convoData, - } = useQuery(GET_CONVERSATION_DETAILS, { - variables: {conversationId: selectedConversation}, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +export function ChatConversationContainer({ bodyshop, selectedConversation }) { + const { + loading: convoLoading, + error: convoError, + data: convoData + } = useQuery(GET_CONVERSATION_DETAILS, { + variables: { conversationId: selectedConversation }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - const {loading, error, data} = useSubscription( - CONVERSATION_SUBSCRIPTION_BY_PK, - { - variables: {conversationId: selectedConversation}, + const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, { + variables: { conversationId: selectedConversation } + }); + + const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); + + const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, { + variables: { conversationId: selectedConversation }, + refetchQueries: ["UNREAD_CONVERSATION_COUNT"], + update(cache) { + cache.modify({ + id: cache.identify({ + __typename: "conversations", + id: selectedConversation + }), + fields: { + messages_aggregate(cached) { + return { aggregate: { count: 0 } }; + } } - ); + }); + } + }); - const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); + const unreadCount = + data && + data.messages && + data.messages.reduce((acc, val) => { + return !val.read && !val.isoutbound ? acc + 1 : acc; + }, 0); - const [markConversationRead] = useMutation( - MARK_MESSAGES_AS_READ_BY_CONVERSATION, - { - variables: {conversationId: selectedConversation}, - refetchQueries: ["UNREAD_CONVERSATION_COUNT"], - update(cache) { - cache.modify({ - id: cache.identify({ - __typename: "conversations", - id: selectedConversation, - }), - fields: { - messages_aggregate(cached) { - return {aggregate: {count: 0}}; - }, - }, - }); - }, - } - ); + const handleMarkConversationAsRead = async () => { + if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) { + setMarkingAsReadInProgress(true); + await markConversationRead({}); + await axios.post("/sms/markConversationRead", { + conversationid: selectedConversation, + imexshopid: bodyshop.imexshopid + }); + setMarkingAsReadInProgress(false); + } + }; - const unreadCount = - data && - data.messages && - data.messages.reduce((acc, val) => { - return !val.read && !val.isoutbound ? acc + 1 : acc; - }, 0); - - const handleMarkConversationAsRead = async () => { - if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) { - setMarkingAsReadInProgress(true); - await markConversationRead({}); - await axios.post("/sms/markConversationRead", { - conversationid: selectedConversation, - imexshopid: bodyshop.imexshopid, - }); - setMarkingAsReadInProgress(false); - } - }; - - return ( - - ); + return ( + + ); } diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx index 95cb1af4f..7157d02c8 100644 --- a/client/src/components/chat-label/chat-label.component.jsx +++ b/client/src/components/chat-label/chat-label.component.jsx @@ -1,68 +1,59 @@ -import {PlusOutlined} from "@ant-design/icons"; -import {useMutation} from "@apollo/client"; -import {Input, notification, Spin, Tag, Tooltip} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {UPDATE_CONVERSATION_LABEL} from "../../graphql/conversations.queries"; +import { PlusOutlined } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Input, notification, Spin, Tag, Tooltip } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; -export default function ChatLabel({conversation}) { - const [loading, setLoading] = useState(false); - const [editing, setEditing] = useState(false); - const [value, setValue] = useState(conversation.label); +export default function ChatLabel({ conversation }) { + const [loading, setLoading] = useState(false); + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(conversation.label); - const {t} = useTranslation(); - const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL); + const { t } = useTranslation(); + const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL); - const handleSave = async () => { - setLoading(true); - try { - const response = await updateLabel({ - variables: {id: conversation.id, label: value}, - }); - if (response.errors) { - notification["error"]({ - message: t("messages.errors.updatinglabel", { - error: JSON.stringify(response.errors), - }), - }); - } else { - setEditing(false); - } - } catch (error) { - notification["error"]({ - message: t("messages.errors.updatinglabel", { - error: JSON.stringify(error), - }), - }); - } finally { - setLoading(false); - } - }; - if (editing) { - return ( - - setValue(e.target.value)} - onBlur={handleSave} - allowClear - /> - {loading && } - - ); - } else { - return conversation.label && conversation.label.trim() !== "" ? ( - setEditing(true)}> - {conversation.label} - - ) : ( - - setEditing(true)} - /> - - ); + const handleSave = async () => { + setLoading(true); + try { + const response = await updateLabel({ + variables: { id: conversation.id, label: value } + }); + if (response.errors) { + notification["error"]({ + message: t("messages.errors.updatinglabel", { + error: JSON.stringify(response.errors) + }) + }); + } else { + setEditing(false); + } + } catch (error) { + notification["error"]({ + message: t("messages.errors.updatinglabel", { + error: JSON.stringify(error) + }) + }); + } finally { + setLoading(false); } + }; + if (editing) { + return ( + + setValue(e.target.value)} onBlur={handleSave} allowClear /> + {loading && } + + ); + } else { + return conversation.label && conversation.label.trim() !== "" ? ( + setEditing(true)}> + {conversation.label} + + ) : ( + + setEditing(true)} /> + + ); + } } diff --git a/client/src/components/chat-media-selector/chat-media-selector.component.jsx b/client/src/components/chat-media-selector/chat-media-selector.component.jsx index c2f32883f..0526fde43 100644 --- a/client/src/components/chat-media-selector/chat-media-selector.component.jsx +++ b/client/src/components/chat-media-selector/chat-media-selector.component.jsx @@ -1,100 +1,82 @@ -import {PictureFilled} from "@ant-design/icons"; -import {useQuery} from "@apollo/client"; -import {Badge, Popover} from "antd"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { PictureFilled } from "@ant-design/icons"; +import { useQuery } from "@apollo/client"; +import { Badge, Popover } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; -import JobDocumentsLocalGalleryExternal - from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; +import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); -export function ChatMediaSelector({ - bodyshop, - selectedMedia, - setSelectedMedia, - conversation, - }) { - const {t} = useTranslation(); - const [open, setOpen] = useState(false); +export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); - const {loading, error, data} = useQuery(GET_DOCUMENTS_BY_JOB, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - variables: { - jobId: - conversation.job_conversations[0] && - conversation.job_conversations[0].jobid, - }, + const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + variables: { + jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid + }, - skip: - !open || - !conversation.job_conversations || - conversation.job_conversations.length === 0, - }); + skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0 + }); - const handleVisibleChange = (change) => { - setOpen(change); - }; + const handleVisibleChange = (change) => { + setOpen(change); + }; - useEffect(() => { - setSelectedMedia([]); - }, [setSelectedMedia, conversation]); + useEffect(() => { + setSelectedMedia([]); + }, [setSelectedMedia, conversation]); - const content = ( - - {loading && } - {error && } - {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( - {t("messaging.labels.maxtenimages")} - ) : null} - {!bodyshop.uselocalmediaserver && data && ( - - )} - {bodyshop.uselocalmediaserver && open && ( - - )} - - ); + const content = ( + + {loading && } + {error && } + {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( + {t("messaging.labels.maxtenimages")} + ) : null} + {!bodyshop.uselocalmediaserver && data && ( + + )} + {bodyshop.uselocalmediaserver && open && ( + + )} + + ); - return ( - {t("messaging.errors.noattachedjobs")} - ) : ( - content - ) - } - title={t("messaging.labels.selectmedia")} - trigger="click" - open={open} - onOpenChange={handleVisibleChange} - > - s.isSelected).length}> - - - - ); + return ( + {t("messaging.errors.noattachedjobs")} : content + } + title={t("messaging.labels.selectmedia")} + trigger="click" + open={open} + onOpenChange={handleVisibleChange} + > + s.isSelected).length}> + + + + ); } diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index e32580900..55aa81dc0 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -1,113 +1,106 @@ import Icon from "@ant-design/icons"; -import {Tooltip} from "antd"; +import { Tooltip } from "antd"; import i18n from "i18next"; import dayjs from "../../utils/day"; -import React, {useEffect, useRef} from "react"; -import {MdDone, MdDoneAll} from "react-icons/md"; -import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; +import React, { useEffect, useRef } from "react"; +import { MdDone, MdDoneAll } from "react-icons/md"; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; import "./chat-message-list.styles.scss"; -export default function ChatMessageListComponent({messages}) { - const virtualizedListRef = useRef(null); +export default function ChatMessageListComponent({ messages }) { + const virtualizedListRef = useRef(null); - const _cache = new CellMeasurerCache({ - fixedWidth: true, - // minHeight: 50, - defaultHeight: 100, - }); + const _cache = new CellMeasurerCache({ + fixedWidth: true, + // minHeight: 50, + defaultHeight: 100 + }); - const scrollToBottom = (renderedrows) => { - //console.log("Scrolling to", messages.length); - // !!virtualizedListRef.current && - // virtualizedListRef.current.scrollToRow(messages.length); - // Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179 - //Scrolling does not work on this version of React. - }; + const scrollToBottom = (renderedrows) => { + //console.log("Scrolling to", messages.length); + // !!virtualizedListRef.current && + // virtualizedListRef.current.scrollToRow(messages.length); + // Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179 + //Scrolling does not work on this version of React. + }; - useEffect(scrollToBottom, [messages]); - - const _rowRenderer = ({index, key, parent, style}) => { - return ( - - {({measure, registerChild}) => ( - - - {MessageRender(messages[index])} - {StatusRender(messages[index].status)} - - {messages[index].isoutbound && ( - - {i18n.t("messaging.labels.sentby", { - by: messages[index].userid, - time: dayjs(messages[index].created_at).format( - "MM/DD/YYYY @ hh:mm a" - ), - })} - - )} - - )} - - ); - }; + useEffect(scrollToBottom, [messages]); + const _rowRenderer = ({ index, key, parent, style }) => { return ( - - - {({height, width}) => ( - - )} - - + + {({ measure, registerChild }) => ( + + + {MessageRender(messages[index])} + {StatusRender(messages[index].status)} + + {messages[index].isoutbound && ( + + {i18n.t("messaging.labels.sentby", { + by: messages[index].userid, + time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a") + })} + + )} + + )} + ); + }; + + return ( + + + {({ height, width }) => ( + + )} + + + ); } const MessageRender = (message) => { - return ( - - - {message.image_path && - message.image_path.map((i, idx) => ( - - - - - - ))} - {message.text} + return ( + + + {message.image_path && + message.image_path.map((i, idx) => ( + + + + - - ); + ))} + {message.text} + + + ); }; const StatusRender = (status) => { - switch (status) { - case "sent": - return ; - case "delivered": - return ; - default: - return null; - } + switch (status) { + case "sent": + return ; + case "delivered": + return ; + default: + return null; + } }; diff --git a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx index 61480c310..b0b6054e4 100644 --- a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx +++ b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx @@ -1,55 +1,49 @@ -import {PlusCircleFilled} from "@ant-design/icons"; -import {Button, Form, Popover} from "antd"; +import { PlusCircleFilled } from "@ant-design/icons"; +import { Button, Form, Popover } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {openChatByPhone} from "../../redux/messaging/messaging.actions"; -import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { openChatByPhone } from "../../redux/messaging/messaging.actions"; +import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser + //currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), + openChatByPhone: (phone) => dispatch(openChatByPhone(phone)) }); -export function ChatNewConversation({openChatByPhone}) { - const {t} = useTranslation(); - const [form] = Form.useForm(); - const handleFinish = (values) => { - openChatByPhone({phone_num: values.phoneNumber}); - form.resetFields(); - }; +export function ChatNewConversation({ openChatByPhone }) { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const handleFinish = (values) => { + openChatByPhone({ phone_num: values.phoneNumber }); + form.resetFields(); + }; - const popContent = ( - - - - PhoneItemFormatterValidation(getFieldValue, "phoneNumber"), - ]} - > - - - - {t("messaging.actions.new")} - - - - ); + const popContent = ( + + + PhoneItemFormatterValidation(getFieldValue, "phoneNumber")]} + > + + + + {t("messaging.actions.new")} + + + + ); - return ( - - - - ); + return ( + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ChatNewConversation); +export default connect(mapStateToProps, mapDispatchToProps)(ChatNewConversation); diff --git a/client/src/components/chat-open-button/chat-open-button.component.jsx b/client/src/components/chat-open-button/chat-open-button.component.jsx index 539c5ec36..d2f0de528 100644 --- a/client/src/components/chat-open-button/chat-open-button.component.jsx +++ b/client/src/components/chat-open-button/chat-open-button.component.jsx @@ -1,54 +1,47 @@ -import {notification} from "antd"; +import { notification } from "antd"; import parsePhoneNumber from "libphonenumber-js"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {openChatByPhone} from "../../redux/messaging/messaging.actions"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { openChatByPhone } from "../../redux/messaging/messaging.actions"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {searchingForConversation} from "../../redux/messaging/messaging.selectors"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { searchingForConversation } from "../../redux/messaging/messaging.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - searchingForConversation: searchingForConversation, + bodyshop: selectBodyshop, + searchingForConversation: searchingForConversation }); const mapDispatchToProps = (dispatch) => ({ - openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), + openChatByPhone: (phone) => dispatch(openChatByPhone(phone)) }); -export function ChatOpenButton({ - bodyshop, - searchingForConversation, - phone, - jobid, - openChatByPhone, - }) { - const {t} = useTranslation(); - if (!phone) return <>>; +export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) { + const { t } = useTranslation(); + if (!phone) return <>>; - if (!bodyshop.messagingservicesid) - return {phone}; + if (!bodyshop.messagingservicesid) return {phone}; - return ( - { - e.stopPropagation(); - const p = parsePhoneNumber(phone, "CA"); - if (searchingForConversation) return; //This is to prevent finding the same thing twice. - if (p && p.isValid()) { - openChatByPhone({phone_num: p.formatInternational(), jobid: jobid}); - } else { - notification["error"]({message: t("messaging.error.invalidphone")}); - } - }} - > - {phone} - - ); + return ( + { + e.stopPropagation(); + const p = parsePhoneNumber(phone, "CA"); + if (searchingForConversation) return; //This is to prevent finding the same thing twice. + if (p && p.isValid()) { + openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid }); + } else { + notification["error"]({ message: t("messaging.error.invalidphone") }); + } + }} + > + {phone} + + ); } export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton); diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index 052193b1e..c608f8b11 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -1,13 +1,13 @@ -import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons"; -import {useLazyQuery, useQuery} from "@apollo/client"; -import {Badge, Card, Col, Row, Space, Tag, Tooltip, Typography} from "antd"; -import React, {useCallback, useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries"; -import {toggleChatVisible} from "../../redux/messaging/messaging.actions"; -import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors"; +import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; +import { useLazyQuery, useQuery } from "@apollo/client"; +import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; +import React, { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries"; +import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; +import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component"; import ChatConversationContainer from "../chat-conversation/chat-conversation.container"; import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; @@ -15,119 +15,102 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import "./chat-popup.styles.scss"; const mapStateToProps = createStructuredSelector({ - selectedConversation: selectSelectedConversation, - chatVisible: selectChatVisible, + selectedConversation: selectSelectedConversation, + chatVisible: selectChatVisible }); const mapDispatchToProps = (dispatch) => ({ - toggleChatVisible: () => dispatch(toggleChatVisible()), + toggleChatVisible: () => dispatch(toggleChatVisible()) }); -export function ChatPopupComponent({ - chatVisible, - selectedConversation, - toggleChatVisible, - }) { - const {t} = useTranslation(); - const [pollInterval, setpollInterval] = useState(0); +export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) { + const { t } = useTranslation(); + const [pollInterval, setpollInterval] = useState(0); - const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - ...(pollInterval > 0 ? {pollInterval} : {}), - }); + const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + ...(pollInterval > 0 ? { pollInterval } : {}) + }); - const [getConversations, {loading, data, refetch, fetchMore}] = - useLazyQuery(CONVERSATION_LIST_QUERY, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - skip: !chatVisible, - ...(pollInterval > 0 ? {pollInterval} : {}), - }); + const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + skip: !chatVisible, + ...(pollInterval > 0 ? { pollInterval } : {}) + }); - const fcmToken = sessionStorage.getItem("fcmtoken"); + const fcmToken = sessionStorage.getItem("fcmtoken"); - useEffect(() => { - if (fcmToken) { - setpollInterval(0); - } else { - setpollInterval(60000); + useEffect(() => { + if (fcmToken) { + setpollInterval(0); + } else { + setpollInterval(60000); + } + }, [fcmToken]); + + useEffect(() => { + if (chatVisible) + getConversations({ + variables: { + offset: 0 } - }, [fcmToken]); + }); + }, [chatVisible, getConversations]); - useEffect(() => { - if (chatVisible) - getConversations({ - variables: { - offset: 0, - }, - }); - }, [chatVisible, getConversations]); + const loadMoreConversations = useCallback(() => { + if (data) + fetchMore({ + variables: { + offset: data.conversations.length + } + }); + }, [data, fetchMore]); - const loadMoreConversations = useCallback(() => { - if (data) - fetchMore({ - variables: { - offset: data.conversations.length, - }, - }); - }, [data, fetchMore]); + const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; - const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; + return ( + + + {chatVisible ? ( + + + {t("messaging.labels.messaging")} + + + + + refetch()} /> + {pollInterval > 0 && {t("messaging.labels.nopush")}} + + toggleChatVisible()} + style={{ position: "absolute", right: ".5rem", top: ".5rem" }} + /> - return ( - - - {chatVisible ? ( - - - - {t("messaging.labels.messaging")} - - - - - - refetch()} - /> - {pollInterval > 0 && ( - {t("messaging.labels.nopush")} - )} - - toggleChatVisible()} - style={{position: "absolute", right: ".5rem", top: ".5rem"}} - /> - - - - {loading ? ( - - ) : ( - - )} - - - {selectedConversation ? : null} - - - + + + {loading ? ( + ) : ( - toggleChatVisible()} - style={{cursor: "pointer"}} - > - - {t("messaging.labels.messaging")} - + )} - - - ); + + {selectedConversation ? : null} + + + ) : ( + toggleChatVisible()} style={{ cursor: "pointer" }}> + + {t("messaging.labels.messaging")} + + )} + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent); diff --git a/client/src/components/chat-presets/chat-presets.component.jsx b/client/src/components/chat-presets/chat-presets.component.jsx index 5fee9c0fe..63b79c373 100644 --- a/client/src/components/chat-presets/chat-presets.component.jsx +++ b/client/src/components/chat-presets/chat-presets.component.jsx @@ -1,38 +1,34 @@ -import {PlusCircleOutlined} from "@ant-design/icons"; -import {Dropdown} from "antd"; +import { PlusCircleOutlined } from "@ant-design/icons"; +import { Dropdown } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setMessage} from "../../redux/messaging/messaging.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setMessage } from "../../redux/messaging/messaging.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) - setMessage: (message) => dispatch(setMessage(message)), + //setUserLanguage: language => dispatch(setUserLanguage(language)) + setMessage: (message) => dispatch(setMessage(message)) }); -export function ChatPresetsComponent({bodyshop, setMessage, className}) { +export function ChatPresetsComponent({ bodyshop, setMessage, className }) { + const items = bodyshop.md_messaging_presets.map((i, idx) => ({ + key: idx, + label: i.label, + onClick: () => setMessage(i.text) + })); - const items = bodyshop.md_messaging_presets.map((i, idx) => ({ - key: idx, - label: (i.label), - onClick: () => setMessage(i.text), - })); - - return ( - - - - - - ); + return ( + + + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ChatPresetsComponent); +export default connect(mapStateToProps, mapDispatchToProps)(ChatPresetsComponent); diff --git a/client/src/components/chat-print-button/chat-print-button.component.jsx b/client/src/components/chat-print-button/chat-print-button.component.jsx index 2e61c0002..0b175b769 100644 --- a/client/src/components/chat-print-button/chat-print-button.component.jsx +++ b/client/src/components/chat-print-button/chat-print-button.component.jsx @@ -1,46 +1,46 @@ -import {MailOutlined, PrinterOutlined} from "@ant-design/icons"; -import {Space, Spin} from "antd"; -import React, {useState} from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setEmailOptions} from "../../redux/email/email.actions"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { MailOutlined, PrinterOutlined } from "@ant-design/icons"; +import { Space, Spin } from "antd"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setEmailOptions } from "../../redux/email/email.actions"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = (dispatch) => ({ - setEmailOptions: (e) => dispatch(setEmailOptions(e)), + setEmailOptions: (e) => dispatch(setEmailOptions(e)) }); -export function ChatPrintButton({conversation}) { - const [loading, setLoading] = useState(false); +export function ChatPrintButton({ conversation }) { + const [loading, setLoading] = useState(false); - const generateDocument = (type) => { - setLoading(true); - GenerateDocument( - { - name: TemplateList("messaging").conversation_list.key, - variables: {id: conversation.id}, - }, - { - subject: TemplateList("messaging").conversation_list.subject, - }, - type, - conversation.id - ).catch(e => { - console.warn('Something went wrong generating a document.'); - }); - setLoading(false); - } + const generateDocument = (type) => { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id } + }, + { + subject: TemplateList("messaging").conversation_list.subject + }, + type, + conversation.id + ).catch((e) => { + console.warn("Something went wrong generating a document."); + }); + setLoading(false); + }; - return ( - - generateDocument('p')}/> - generateDocument('e')}/> - {loading && } - - ); + return ( + + generateDocument("p")} /> + generateDocument("e")} /> + {loading && } + + ); } export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton); diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index cae683876..918be1b5f 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -1,111 +1,101 @@ -import {LoadingOutlined, SendOutlined} from "@ant-design/icons"; -import {Input, Spin} from "antd"; -import React, {useEffect, useRef, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {sendMessage, setMessage,} from "../../redux/messaging/messaging.actions"; -import {selectIsSending, selectMessage,} from "../../redux/messaging/messaging.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; +import { Input, Spin } from "antd"; +import React, { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { sendMessage, setMessage } from "../../redux/messaging/messaging.actions"; +import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - isSending: selectIsSending, - message: selectMessage, + bodyshop: selectBodyshop, + isSending: selectIsSending, + message: selectMessage }); const mapDispatchToProps = (dispatch) => ({ - sendMessage: (message) => dispatch(sendMessage(message)), - setMessage: (message) => dispatch(setMessage(message)), + sendMessage: (message) => dispatch(sendMessage(message)), + setMessage: (message) => dispatch(setMessage(message)) }); -function ChatSendMessageComponent({ - conversation, - bodyshop, - sendMessage, - isSending, - message, - setMessage, - }) { - const inputArea = useRef(null); - const [selectedMedia, setSelectedMedia] = useState([]); - useEffect(() => { - inputArea.current.focus(); - }, [isSending, setMessage]); +function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) { + const inputArea = useRef(null); + const [selectedMedia, setSelectedMedia] = useState([]); + useEffect(() => { + inputArea.current.focus(); + }, [isSending, setMessage]); - const {t} = useTranslation(); + const { t } = useTranslation(); - const handleEnter = () => { - const selectedImages = selectedMedia.filter((i) => i.isSelected); - if ((message === "" || !message) && selectedImages.length === 0) return; - logImEXEvent("messaging_send_message"); + const handleEnter = () => { + const selectedImages = selectedMedia.filter((i) => i.isSelected); + if ((message === "" || !message) && selectedImages.length === 0) return; + logImEXEvent("messaging_send_message"); - if (selectedImages.length < 11) { - sendMessage({ - to: conversation.phone_num, - body: message || "", - messagingServiceSid: bodyshop.messagingservicesid, - conversationid: conversation.id, - selectedMedia: selectedImages, - imexshopid: bodyshop.imexshopid, - }); - setSelectedMedia( - selectedMedia.map((i) => { - return {...i, isSelected: false}; - }) - ); - } - }; + if (selectedImages.length < 11) { + sendMessage({ + to: conversation.phone_num, + body: message || "", + messagingServiceSid: bodyshop.messagingservicesid, + conversationid: conversation.id, + selectedMedia: selectedImages, + imexshopid: bodyshop.imexshopid + }); + setSelectedMedia( + selectedMedia.map((i) => { + return { ...i, isSelected: false }; + }) + ); + } + }; - return ( - - - - + return ( + + + + setMessage(e.target.value)} - onPressEnter={(event) => { - event.preventDefault(); - if (!!!event.shiftKey) handleEnter(); - }} + className="imex-flex-row__margin imex-flex-row__grow" + allowClear + autoFocus + ref={inputArea} + autoSize={{ minRows: 1, maxRows: 4 }} + value={message} + disabled={isSending} + placeholder={t("messaging.labels.typeamessage")} + onChange={(e) => setMessage(e.target.value)} + onPressEnter={(event) => { + event.preventDefault(); + if (!!!event.shiftKey) handleEnter(); + }} /> - - - } - /> - - ); + + + } + /> + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ChatSendMessageComponent); +export default connect(mapStateToProps, mapDispatchToProps)(ChatSendMessageComponent); diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx index 004b28a3c..f40c5c85e 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx @@ -1,45 +1,35 @@ -import {CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons"; -import {Empty, Select, Space} from "antd"; +import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons"; +import { Empty, Select, Space } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component"; +import { useTranslation } from "react-i18next"; +import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; -export default function ChatTagRoComponent({ - roOptions, - loading, - handleSearch, - handleInsertTag, - setOpen, - }) { - const {t} = useTranslation(); +export default function ChatTagRoComponent({ roOptions, loading, handleSearch, handleInsertTag, setOpen }) { + const { t } = useTranslation(); - return ( - - - : } - > - {roOptions.map((item, idx) => ( - - {` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`} - - ))} - - - {loading ? : null} + return ( + + + : } + > + {roOptions.map((item, idx) => ( + + {` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`} + + ))} + + + {loading ? : null} - {loading ? ( - - ) : ( - setOpen(false)}/> - )} - - ); + {loading ? : setOpen(false)} />} + + ); } diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index f2ea134f0..f9b6fa5ad 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -1,66 +1,62 @@ -import {PlusOutlined} from "@ant-design/icons"; -import {useLazyQuery, useMutation} from "@apollo/client"; -import {Tag} from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import { useLazyQuery, useMutation } from "@apollo/client"; +import { Tag } from "antd"; import _ from "lodash"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {INSERT_CONVERSATION_TAG} from "../../graphql/job-conversations.queries"; -import {SEARCH_FOR_JOBS} from "../../graphql/jobs.queries"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; +import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import ChatTagRo from "./chat-tag-ro.component"; -export default function ChatTagRoContainer({conversation}) { - const {t} = useTranslation(); - const [open, setOpen] = useState(false); +export default function ChatTagRoContainer({ conversation }) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); - const [loadRo, {loading, data}] = useLazyQuery(SEARCH_FOR_JOBS); + const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS); - const executeSearch = (v) => { - logImEXEvent("messaging_search_job_tag", {searchTerm: v}); - loadRo(v); - }; + const executeSearch = (v) => { + logImEXEvent("messaging_search_job_tag", { searchTerm: v }); + loadRo(v); + }; - const debouncedExecuteSearch = _.debounce(executeSearch, 500); + const debouncedExecuteSearch = _.debounce(executeSearch, 500); - const handleSearch = (value) => { - debouncedExecuteSearch({variables: {search: value}}); - }; + const handleSearch = (value) => { + debouncedExecuteSearch({ variables: { search: value } }); + }; - const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, { - variables: {conversationId: conversation.id}, - }); + const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, { + variables: { conversationId: conversation.id } + }); - const handleInsertTag = (value, option) => { - logImEXEvent("messaging_add_job_tag"); - insertTag({variables: {jobId: option.key}}); - setOpen(false); - }; + const handleInsertTag = (value, option) => { + logImEXEvent("messaging_add_job_tag"); + insertTag({ variables: { jobId: option.key } }); + setOpen(false); + }; - const existingJobTags = - conversation && - conversation.job_conversations && - conversation.job_conversations.map((i) => i.jobid); + const existingJobTags = + conversation && conversation.job_conversations && conversation.job_conversations.map((i) => i.jobid); - const roOptions = data - ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id)) - : []; + const roOptions = data ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id)) : []; - return ( - - {open ? ( - - ) : ( - setOpen(true)}> - - {t("messaging.actions.link")} - - )} - - ); + return ( + + {open ? ( + + ) : ( + setOpen(true)}> + + {t("messaging.actions.link")} + + )} + + ); } diff --git a/client/src/components/config-form-components/checkbox/checkbox.component.jsx b/client/src/components/config-form-components/checkbox/checkbox.component.jsx index f3945268d..7c5a6c4be 100644 --- a/client/src/components/config-form-components/checkbox/checkbox.component.jsx +++ b/client/src/components/config-form-components/checkbox/checkbox.component.jsx @@ -1,22 +1,22 @@ -import {Checkbox, Form} from "antd"; +import { Checkbox, Form } from "antd"; import React from "react"; -export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) { - const {name, label, required} = formItem; +export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) { + const { name, label, required } = formItem; - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/config-form-components/config-form-components.component.jsx b/client/src/components/config-form-components/config-form-components.component.jsx index c2b513052..abcd6a6b5 100644 --- a/client/src/components/config-form-components/config-form-components.component.jsx +++ b/client/src/components/config-form-components/config-form-components.component.jsx @@ -1,18 +1,18 @@ import React from "react"; import FormTypes from "./config-form-types"; -export default function ConfirmFormComponents({componentList, readOnly}) { - return ( - - {componentList.map((f, idx) => { - const Comp = FormTypes[f.type]; +export default function ConfirmFormComponents({ componentList, readOnly }) { + return ( + + {componentList.map((f, idx) => { + const Comp = FormTypes[f.type]; - if (!!Comp) { - return ; - } else { - return Error; - } - })} - - ); + if (!!Comp) { + return ; + } else { + return Error; + } + })} + + ); } diff --git a/client/src/components/config-form-components/config-form-types.js b/client/src/components/config-form-components/config-form-types.js index fa2121ef8..a59a66d91 100644 --- a/client/src/components/config-form-components/config-form-types.js +++ b/client/src/components/config-form-components/config-form-types.js @@ -5,11 +5,11 @@ import Text from "./text/text.component"; import Textarea from "./textarea/textarea.component"; const e = { - checkbox: CheckboxFormItem, - slider: Slider, - text: Text, - textarea: Textarea, - rate: Rate, + checkbox: CheckboxFormItem, + slider: Slider, + text: Text, + textarea: Textarea, + rate: Rate }; export default e; diff --git a/client/src/components/config-form-components/rate/rate.component.jsx b/client/src/components/config-form-components/rate/rate.component.jsx index 2e144b7be..9f7e0189f 100644 --- a/client/src/components/config-form-components/rate/rate.component.jsx +++ b/client/src/components/config-form-components/rate/rate.component.jsx @@ -1,21 +1,21 @@ -import {Form, Rate} from "antd"; +import { Form, Rate } from "antd"; import React from "react"; -export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) { - const {name, label, required} = formItem; +export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) { + const { name, label, required } = formItem; - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/config-form-components/slider/slider.component.jsx b/client/src/components/config-form-components/slider/slider.component.jsx index 0d8da40f7..1fde2bfa4 100644 --- a/client/src/components/config-form-components/slider/slider.component.jsx +++ b/client/src/components/config-form-components/slider/slider.component.jsx @@ -1,21 +1,21 @@ -import {Form, Slider} from "antd"; +import { Form, Slider } from "antd"; import React from "react"; -export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) { - const {name, label, required, min, max} = formItem; +export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) { + const { name, label, required, min, max } = formItem; - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/config-form-components/text/text.component.jsx b/client/src/components/config-form-components/text/text.component.jsx index 9364e1c5d..73394e0c0 100644 --- a/client/src/components/config-form-components/text/text.component.jsx +++ b/client/src/components/config-form-components/text/text.component.jsx @@ -1,20 +1,20 @@ -import {Form, Input} from "antd"; +import { Form, Input } from "antd"; import React from "react"; -export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) { - const {name, label, required} = formItem; - return ( - - - - ); +export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) { + const { name, label, required } = formItem; + return ( + + + + ); } diff --git a/client/src/components/config-form-components/textarea/textarea.component.jsx b/client/src/components/config-form-components/textarea/textarea.component.jsx index f707e4298..c9bb19781 100644 --- a/client/src/components/config-form-components/textarea/textarea.component.jsx +++ b/client/src/components/config-form-components/textarea/textarea.component.jsx @@ -1,21 +1,21 @@ -import {Form, Input} from "antd"; +import { Form, Input } from "antd"; import React from "react"; -export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) { - const {name, label, required, rows} = formItem; +export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) { + const { name, label, required, rows } = formItem; - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/conflict/conflict.component.jsx b/client/src/components/conflict/conflict.component.jsx index f9087b1b7..659e64080 100644 --- a/client/src/components/conflict/conflict.component.jsx +++ b/client/src/components/conflict/conflict.component.jsx @@ -1,28 +1,36 @@ import React from "react"; -import {Button, Result} from "antd"; -import {useTranslation} from "react-i18next"; -import InstanceRenderManager from '../../utils/instanceRenderMgr'; +import { Button, Result } from "antd"; +import { useTranslation } from "react-i18next"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; export default function ConflictComponent() { - const {t} = useTranslation(); - return ( - - - {t("general.labels.instanceconflictext",{app: InstanceRenderManager({imex:'$t(titles.imexonline)', rome: '$t(titles.romeonline)', promanager: '$t(titles.promanager)'})})} - { - window.location.reload(); - }} - > - {t("general.actions.refresh")} - - - } - /> - - ); + const { t } = useTranslation(); + return ( + + + + {t("general.labels.instanceconflictext", { + app: InstanceRenderManager({ + imex: "$t(titles.imexonline)", + rome: "$t(titles.romeonline)", + promanager: "$t(titles.promanager)" + }) + })} + + { + window.location.reload(); + }} + > + {t("general.actions.refresh")} + + + } + /> + + ); } diff --git a/client/src/components/contract-cars/contract-cars.component.jsx b/client/src/components/contract-cars/contract-cars.component.jsx index 9868b0ad9..830dd5531 100644 --- a/client/src/components/contract-cars/contract-cars.component.jsx +++ b/client/src/components/contract-cars/contract-cars.component.jsx @@ -1,152 +1,127 @@ -import {Card, Input, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {alphaSort} from "../../utils/sorters"; +import { Card, Input, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { alphaSort } from "../../utils/sorters"; -export default function ContractsCarsComponent({ - loading, - data, - selectedCarId, - handleSelect, - }) { - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {text: ""}, - search: "", - }); +export default function ContractsCarsComponent({ loading, data, selectedCarId, handleSelect }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" }, + search: "" + }); - const {t} = useTranslation(); + const { t } = useTranslation(); - const columns = [ - { - title: t("courtesycars.fields.fleetnumber"), - dataIndex: "fleetnumber", - key: "fleetnumber", - sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber), - sortOrder: - state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.status"), - dataIndex: "status", - key: "status", - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - render: (text, record) => {t(record.status)}, - }, - { - title: t("courtesycars.fields.readiness"), - dataIndex: "readiness", - key: "readiness", - sorter: (a, b) => alphaSort(a.readiness, b.readiness), - sortOrder: - state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, - render: (text, record) => t(record.readiness), - }, - { - title: t("courtesycars.fields.year"), - dataIndex: "year", - key: "year", - sorter: (a, b) => alphaSort(a.year, b.year), - sortOrder: - state.sortedInfo.columnKey === "year" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.make"), - dataIndex: "make", - key: "make", - sorter: (a, b) => alphaSort(a.make, b.make), - sortOrder: - state.sortedInfo.columnKey === "make" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.model"), - dataIndex: "model", - key: "model", - sorter: (a, b) => alphaSort(a.model, b.model), - sortOrder: - state.sortedInfo.columnKey === "model" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.color"), - dataIndex: "color", - key: "color", - sorter: (a, b) => alphaSort(a.color, b.color), - sortOrder: - state.sortedInfo.columnKey === "color" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.plate"), - dataIndex: "plate", - key: "plate", - sorter: (a, b) => alphaSort(a.plate, b.plate), - sortOrder: - state.sortedInfo.columnKey === "plate" && state.sortedInfo.order, - }, - ]; + const columns = [ + { + title: t("courtesycars.fields.fleetnumber"), + dataIndex: "fleetnumber", + key: "fleetnumber", + sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber), + sortOrder: state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + render: (text, record) => {t(record.status)} + }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + sortOrder: state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness) + }, + { + title: t("courtesycars.fields.year"), + dataIndex: "year", + key: "year", + sorter: (a, b) => alphaSort(a.year, b.year), + sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.make"), + dataIndex: "make", + key: "make", + sorter: (a, b) => alphaSort(a.make, b.make), + sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.model"), + dataIndex: "model", + key: "model", + sorter: (a, b) => alphaSort(a.model, b.model), + sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.color"), + dataIndex: "color", + key: "color", + sorter: (a, b) => alphaSort(a.color, b.color), + sortOrder: state.sortedInfo.columnKey === "color" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.plate"), + dataIndex: "plate", + key: "plate", + sorter: (a, b) => alphaSort(a.plate, b.plate), + sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const filteredData = - state.search === "" - ? data - : data.filter( - (cc) => - (cc.fleetnumber || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.status || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.year || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.make || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.model || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.color || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (cc.plate || "").toLowerCase().includes(state.search.toLowerCase()) - ); + const filteredData = + state.search === "" + ? data + : data.filter( + (cc) => + (cc.fleetnumber || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.status || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.year || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.make || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.model || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.color || "").toLowerCase().includes(state.search.toLowerCase()) || + (cc.plate || "").toLowerCase().includes(state.search.toLowerCase()) + ); - return ( - setState({...state, search: e.target.value})} - /> + return ( + setState({ ...state, search: e.target.value })} + /> + } + > + { + return { + onClick: (event) => { + handleSelect(record); } - > - { - return { - onClick: (event) => { - handleSelect(record); - }, - }; - }} - /> - - ); + }; + }} + /> + + ); } diff --git a/client/src/components/contract-cars/contract-cars.container.jsx b/client/src/components/contract-cars/contract-cars.container.jsx index 9d5f23355..24364313d 100644 --- a/client/src/components/contract-cars/contract-cars.container.jsx +++ b/client/src/components/contract-cars/contract-cars.container.jsx @@ -1,37 +1,37 @@ -import {useQuery} from "@apollo/client"; +import { useQuery } from "@apollo/client"; import dayjs from "../../utils/day"; import React from "react"; -import {QUERY_AVAILABLE_CC} from "../../graphql/courtesy-car.queries"; +import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries"; import AlertComponent from "../alert/alert.component"; import ContractCarsComponent from "./contract-cars.component"; -export default function ContractCarsContainer({selectedCarState, form}) { - const {loading, error, data} = useQuery(QUERY_AVAILABLE_CC, { - variables: {today: dayjs().format("YYYY-MM-DD")}, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", +export default function ContractCarsContainer({ selectedCarState, form }) { + const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, { + variables: { today: dayjs().format("YYYY-MM-DD") }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + + const [selectedCar, setSelectedCar] = selectedCarState; + + const handleSelect = (record) => { + setSelectedCar(record); + + form.setFieldsValue({ + kmstart: record.mileage, + dailyrate: record.dailycost, + fuelout: record.fuel, + damage: record.damage }); + }; - const [selectedCar, setSelectedCar] = selectedCarState; - - const handleSelect = (record) => { - setSelectedCar(record); - - form.setFieldsValue({ - kmstart: record.mileage, - dailyrate: record.dailycost, - fuelout: record.fuel, - damage: record.damage, - }); - }; - - if (error) return ; - return ( - - ); + if (error) return ; + return ( + + ); } diff --git a/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx b/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx index b267d0dc6..ee8d01ed6 100644 --- a/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx +++ b/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx @@ -1,397 +1,381 @@ -import {useMutation} from "@apollo/client"; -import {Button, Form, InputNumber, notification, Popover, Radio, Select, Space,} from "antd"; +import { useMutation } from "@apollo/client"; +import { Button, Form, InputNumber, notification, Popover, Radio, Select, Space } from "antd"; import axios from "axios"; import dayjs from "../../utils/day"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useNavigate} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {INSERT_NEW_JOB} from "../../graphql/jobs.queries"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { INSERT_NEW_JOB } from "../../graphql/jobs.queries"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ContractConvertToRo({ - bodyshop, - currentUser, - contract, - disabled, - }) { - const {t} = useTranslation(); - const [open, setOpen] = useState(false); - const [loading, setLoading] = useState(false); - const [insertJob] = useMutation(INSERT_NEW_JOB); - const history = useNavigate(); +export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled }) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [insertJob] = useMutation(INSERT_NEW_JOB); + const history = useNavigate(); - const handleFinish = async (values) => { - setLoading(true); + const handleFinish = async (values) => { + setLoading(true); - const contractLength = dayjs(contract.actualreturn).diff( - dayjs(contract.start), - "day" - ); - const billingLines = []; - if (contractLength > 0) - billingLines.push({ - manual_line: true, - unq_seq: 1, - line_no: 1, - line_ref: 1, - line_desc: t("contracts.fields.dailyrate"), - db_price: contract.dailyrate, - act_price: contract.dailyrate, - part_qty: contractLength, - part_type: "CCDR", - tax_part: true, - mod_lb_hrs: 0, - db_ref: "io-ccdr", - // mod_lbr_ty: "PAL", - }); - const mileageDiff = - contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength; - if (mileageDiff > 0) { - billingLines.push({ - manual_line: true, - unq_seq: 2, - line_no: 2, - line_ref: 2, - line_desc: "Fuel Surcharge", - db_price: contract.excesskmrate, - act_price: contract.excesskmrate, - part_type: "CCM", - part_qty: mileageDiff, - tax_part: true, - db_ref: "io-ccm", - mod_lb_hrs: 0, - }); + const contractLength = dayjs(contract.actualreturn).diff(dayjs(contract.start), "day"); + const billingLines = []; + if (contractLength > 0) + billingLines.push({ + manual_line: true, + unq_seq: 1, + line_no: 1, + line_ref: 1, + line_desc: t("contracts.fields.dailyrate"), + db_price: contract.dailyrate, + act_price: contract.dailyrate, + part_qty: contractLength, + part_type: "CCDR", + tax_part: true, + mod_lb_hrs: 0, + db_ref: "io-ccdr" + // mod_lbr_ty: "PAL", + }); + const mileageDiff = contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength; + if (mileageDiff > 0) { + billingLines.push({ + manual_line: true, + unq_seq: 2, + line_no: 2, + line_ref: 2, + line_desc: "Fuel Surcharge", + db_price: contract.excesskmrate, + act_price: contract.excesskmrate, + part_type: "CCM", + part_qty: mileageDiff, + tax_part: true, + db_ref: "io-ccm", + mod_lb_hrs: 0 + }); + } + + if (values.refuelqty > 0) { + billingLines.push({ + manual_line: true, + unq_seq: 3, + line_no: 3, + line_ref: 3, + line_desc: t("contracts.fields.refuelcharge"), + db_price: contract.refuelcharge, + act_price: contract.refuelcharge, + part_qty: values.refuelqty, + part_type: "CCF", + tax_part: true, + db_ref: "io-ccf", + mod_lb_hrs: 0 + }); + } + if (values.applyCleanupCharge) { + billingLines.push({ + manual_line: true, + unq_seq: 4, + line_no: 4, + line_ref: 4, + line_desc: t("contracts.fields.cleanupcharge"), + db_price: contract.cleanupcharge, + act_price: contract.cleanupcharge, + part_qty: 1, + part_type: "CCC", + tax_part: true, + db_ref: "io-ccc", + mod_lb_hrs: 0 + }); + } + if (contract.damagewaiver) { + //Add for cleanup fee. + billingLines.push({ + manual_line: true, + unq_seq: 5, + line_no: 5, + line_ref: 5, + line_desc: t("contracts.fields.damagewaiver"), + db_price: contract.damagewaiver, + act_price: contract.damagewaiver, + part_type: "CCD", + part_qty: 1, + tax_part: true, + db_ref: "io-ccd", + mod_lb_hrs: 0 + }); + } + + const newJob = { + // converted: true, + shopid: bodyshop.id, + ownerid: contract.job.ownerid, + vehicleid: contract.job.vehicleid, + federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100, + state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100, + local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100, + ins_co_nm: values.ins_co_nm, + class: values.class, + converted: true, + clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null, + ownr_fn: contract.job.owner.ownr_fn, + ownr_ln: contract.job.owner.ownr_ln, + ownr_co_nm: contract.job.owner.ownr_co_nm, + ownr_ph1: contract.job.owner.ownr_ph1, + ownr_ea: contract.job.owner.ownr_ea, + v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc, + v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr, + v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc, + v_vin: contract.job.vehicle && contract.job.vehicle.v_vin, + status: bodyshop.md_ro_statuses.default_completed, + notes: { + data: [ + { + text: t("contracts.labels.noteconvertedfrom", { + agreementnumber: contract.agreementnumber + }), + audit: true, + created_by: currentUser.email + } + ] + }, + joblines: { + data: billingLines + }, + parts_tax_rates: { + PAA: { + prt_type: "PAA", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAC: { + prt_type: "PAC", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAL: { + prt_type: "PAL", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAM: { + prt_type: "PAM", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAN: { + prt_type: "PAN", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAR: { + prt_type: "PAR", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + PAS: { + prt_type: "PAS", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + CCDR: { + prt_type: "CCDR", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + CCF: { + prt_type: "CCF", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + CCM: { + prt_type: "CCM", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + CCC: { + prt_type: "CCC", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 + }, + CCD: { + prt_type: "CCD", + prt_discp: 0, + prt_mktyp: false, + prt_mkupp: 0, + prt_tax_in: true, + prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100 } - - if (values.refuelqty > 0) { - billingLines.push({ - manual_line: true, - unq_seq: 3, - line_no: 3, - line_ref: 3, - line_desc: t("contracts.fields.refuelcharge"), - db_price: contract.refuelcharge, - act_price: contract.refuelcharge, - part_qty: values.refuelqty, - part_type: "CCF", - tax_part: true, - db_ref: "io-ccf", - mod_lb_hrs: 0, - }); - } - if (values.applyCleanupCharge) { - billingLines.push({ - manual_line: true, - unq_seq: 4, - line_no: 4, - line_ref: 4, - line_desc: t("contracts.fields.cleanupcharge"), - db_price: contract.cleanupcharge, - act_price: contract.cleanupcharge, - part_qty: 1, - part_type: "CCC", - tax_part: true, - db_ref: "io-ccc", - mod_lb_hrs: 0, - }); - } - if (contract.damagewaiver) { - //Add for cleanup fee. - billingLines.push({ - manual_line: true, - unq_seq: 5, - line_no: 5, - line_ref: 5, - line_desc: t("contracts.fields.damagewaiver"), - db_price: contract.damagewaiver, - act_price: contract.damagewaiver, - part_type: "CCD", - part_qty: 1, - tax_part: true, - db_ref: "io-ccd", - mod_lb_hrs: 0, - }); - } - - const newJob = { - // converted: true, - shopid: bodyshop.id, - ownerid: contract.job.ownerid, - vehicleid: contract.job.vehicleid, - federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100, - state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100, - local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100, - ins_co_nm: values.ins_co_nm, - class: values.class, - converted: true, - clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null, - ownr_fn: contract.job.owner.ownr_fn, - ownr_ln: contract.job.owner.ownr_ln, - ownr_co_nm: contract.job.owner.ownr_co_nm, - ownr_ph1: contract.job.owner.ownr_ph1, - ownr_ea: contract.job.owner.ownr_ea, - v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc, - v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr, - v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc, - v_vin: contract.job.vehicle && contract.job.vehicle.v_vin, - status: bodyshop.md_ro_statuses.default_completed, - notes: { - data: [ - { - text: t("contracts.labels.noteconvertedfrom", { - agreementnumber: contract.agreementnumber, - }), - audit: true, - created_by: currentUser.email, - }, - ], - }, - joblines: { - data: billingLines, - }, - parts_tax_rates: { - PAA: { - prt_type: "PAA", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAC: { - prt_type: "PAC", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAL: { - prt_type: "PAL", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAM: { - prt_type: "PAM", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAN: { - prt_type: "PAN", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAR: { - prt_type: "PAR", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAS: { - prt_type: "PAS", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - CCDR: { - prt_type: "CCDR", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - CCF: { - prt_type: "CCF", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - CCM: { - prt_type: "CCM", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - CCC: { - prt_type: "CCC", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - CCD: { - prt_type: "CCD", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - }, - }; - - //Calcualte the new job totals. - - const newTotals = ( - await axios.post("/job/totals", { - job: {...newJob, joblines: billingLines}, - }) - ).data; - - newJob.clm_total = newTotals.totals.total_repairs.amount / 100; - newJob.job_totals = newTotals; - - const result = await insertJob({ - variables: {job: [newJob]}, - // refetchQueries: ["GET_JOB_BY_PK"], - // awaitRefetchQueries: true, - }); - - if (!!result.errors) { - notification["error"]({ - message: t("jobs.errors.inserting", { - message: JSON.stringify(result.errors), - }), - }); - } else { - notification["success"]({ - message: t("jobs.successes.created"), - onClick: () => { - history.push( - `/manage/jobs/${result.data.insert_jobs.returning[0].id}` - ); - }, - }); - } - - setOpen(false); - setLoading(false); + } }; - const popContent = ( - - - - - {bodyshop.md_ins_cos.map((s) => ( - - {s.name} - - ))} - - - - - {bodyshop.md_classes.map((s) => ( - - {s} - - ))} - - - - - {t("general.labels.yes")} - {t("general.labels.no")} - - - - - - - - {t("contracts.actions.convertoro")} - - setOpen(false)}> - {t("general.actions.close")} - - - - - ); + //Calcualte the new job totals. - return ( - - - setOpen(true)} - loading={loading} - disabled={!contract.dailyrate || !contract.actualreturn || disabled} - > - {t("contracts.actions.convertoro")} - - - - ); + const newTotals = ( + await axios.post("/job/totals", { + job: { ...newJob, joblines: billingLines } + }) + ).data; + + newJob.clm_total = newTotals.totals.total_repairs.amount / 100; + newJob.job_totals = newTotals; + + const result = await insertJob({ + variables: { job: [newJob] } + // refetchQueries: ["GET_JOB_BY_PK"], + // awaitRefetchQueries: true, + }); + + if (!!result.errors) { + notification["error"]({ + message: t("jobs.errors.inserting", { + message: JSON.stringify(result.errors) + }) + }); + } else { + notification["success"]({ + message: t("jobs.successes.created"), + onClick: () => { + history.push(`/manage/jobs/${result.data.insert_jobs.returning[0].id}`); + } + }); + } + + setOpen(false); + setLoading(false); + }; + + const popContent = ( + + + + + {bodyshop.md_ins_cos.map((s) => ( + + {s.name} + + ))} + + + + + {bodyshop.md_classes.map((s) => ( + + {s} + + ))} + + + + + {t("general.labels.yes")} + {t("general.labels.no")} + + + + + + + + {t("contracts.actions.convertoro")} + + setOpen(false)}>{t("general.actions.close")} + + + + ); + + return ( + + + setOpen(true)} + loading={loading} + disabled={!contract.dailyrate || !contract.actualreturn || disabled} + > + {t("contracts.actions.convertoro")} + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ContractConvertToRo); +export default connect(mapStateToProps, mapDispatchToProps)(ContractConvertToRo); diff --git a/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx b/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx index d95e1b52e..75aaa3c9f 100644 --- a/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx +++ b/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx @@ -1,32 +1,26 @@ -import {Card} from "antd"; +import { Card } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {Link} from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; import DataLabel from "../data-label/data-label.component"; -export default function ContractCourtesyCarBlock({courtesyCar}) { - const {t} = useTranslation(); - return ( - - - - - {(courtesyCar && courtesyCar.fleetnumber) || ""} - - - {(courtesyCar && courtesyCar.plate) || ""} - - - {`${(courtesyCar && courtesyCar.year) || ""} ${ - (courtesyCar && courtesyCar.make) || "" - } ${(courtesyCar && courtesyCar.model) || ""}`} - - - - - ); +export default function ContractCourtesyCarBlock({ courtesyCar }) { + const { t } = useTranslation(); + return ( + + + + + {(courtesyCar && courtesyCar.fleetnumber) || ""} + + {(courtesyCar && courtesyCar.plate) || ""} + + {`${(courtesyCar && courtesyCar.year) || ""} ${ + (courtesyCar && courtesyCar.make) || "" + } ${(courtesyCar && courtesyCar.model) || ""}`} + + + + + ); } diff --git a/client/src/components/contract-form/contract-form-job-prefill.component.jsx b/client/src/components/contract-form/contract-form-job-prefill.component.jsx index 639ba21b4..76f32023f 100644 --- a/client/src/components/contract-form/contract-form-job-prefill.component.jsx +++ b/client/src/components/contract-form/contract-form-job-prefill.component.jsx @@ -1,45 +1,43 @@ -import {useLazyQuery} from "@apollo/client"; -import {Button, notification} from "antd"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {GET_JOB_FOR_CC_CONTRACT} from "../../graphql/jobs.queries"; +import { useLazyQuery } from "@apollo/client"; +import { Button, notification } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { GET_JOB_FOR_CC_CONTRACT } from "../../graphql/jobs.queries"; -export default function ContractCreateJobPrefillComponent({jobId, form}) { - const [call, {loading, error, data}] = useLazyQuery( - GET_JOB_FOR_CC_CONTRACT - ); - const {t} = useTranslation(); +export default function ContractCreateJobPrefillComponent({ jobId, form }) { + const [call, { loading, error, data }] = useLazyQuery(GET_JOB_FOR_CC_CONTRACT); + const { t } = useTranslation(); - const handleClick = () => { - call({variables: {id: jobId}}); - }; + const handleClick = () => { + call({ variables: { id: jobId } }); + }; - useEffect(() => { - if (data) { - form.setFieldsValue({ - driver_dlst: data.jobs_by_pk.ownr_ast, - driver_fn: data.jobs_by_pk.ownr_fn, - driver_ln: data.jobs_by_pk.ownr_ln, - driver_addr1: data.jobs_by_pk.ownr_addr1, - driver_state: data.jobs_by_pk.ownr_st, - driver_city: data.jobs_by_pk.ownr_city, - driver_zip: data.jobs_by_pk.ownr_zip, - driver_ph1: data.jobs_by_pk.ownr_ph1, - }); - } - }, [data, form]); - - if (error) { - notification["error"]({ - message: t("contracts.errors.fetchingjobinfo", { - error: JSON.stringify(error), - }), - }); + useEffect(() => { + if (data) { + form.setFieldsValue({ + driver_dlst: data.jobs_by_pk.ownr_ast, + driver_fn: data.jobs_by_pk.ownr_fn, + driver_ln: data.jobs_by_pk.ownr_ln, + driver_addr1: data.jobs_by_pk.ownr_addr1, + driver_state: data.jobs_by_pk.ownr_st, + driver_city: data.jobs_by_pk.ownr_city, + driver_zip: data.jobs_by_pk.ownr_zip, + driver_ph1: data.jobs_by_pk.ownr_ph1 + }); } + }, [data, form]); - return ( - - {t("contracts.labels.populatefromjob")} - - ); + if (error) { + notification["error"]({ + message: t("contracts.errors.fetchingjobinfo", { + error: JSON.stringify(error) + }) + }); + } + + return ( + + {t("contracts.labels.populatefromjob")} + + ); } diff --git a/client/src/components/contract-form/contract-form.component.jsx b/client/src/components/contract-form/contract-form.component.jsx index ae983a6c6..eeac43701 100644 --- a/client/src/components/contract-form/contract-form.component.jsx +++ b/client/src/components/contract-form/contract-form.component.jsx @@ -1,9 +1,9 @@ -import {WarningFilled} from "@ant-design/icons"; -import {Form, Input, InputNumber, Space} from "antd"; +import { WarningFilled } from "@ant-design/icons"; +import { Form, Input, InputNumber, Space } from "antd"; import dayjs from "../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {DateFormatter} from "../../utils/DateFormatter"; +import { useTranslation } from "react-i18next"; +import { DateFormatter } from "../../utils/DateFormatter"; //import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component"; import ContractStatusSelector from "../contract-status-select/contract-status-select.component"; import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component"; @@ -11,361 +11,310 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; -import InputPhone, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component"; +import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ContractFormJobPrefill from "./contract-form-job-prefill.component"; -export default function ContractFormComponent({ - form, - create = false, - selectedJobState, - selectedCar, - }) { - const {t} = useTranslation(); - return ( - - - - {create ? null : ( - - - - )} - - - - - - - {create ? null : ( - - - - )} - {create && ( - p.scheduledreturn !== c.scheduledreturn} - > - {() => { - const insuranceOver = - selectedCar && - selectedCar.insuranceexpires && - dayjs(selectedCar.insuranceexpires) - .endOf("day") - .isBefore(dayjs(form.getFieldValue("scheduledreturn"))); - if (insuranceOver) - return ( - +export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) { + const { t } = useTranslation(); + return ( + + + + {create ? null : ( + + + + )} + + + + + + + {create ? null : ( + + + + )} + {create && ( + p.scheduledreturn !== c.scheduledreturn}> + {() => { + const insuranceOver = + selectedCar && + selectedCar.insuranceexpires && + dayjs(selectedCar.insuranceexpires) + .endOf("day") + .isBefore(dayjs(form.getFieldValue("scheduledreturn"))); + if (insuranceOver) + return ( + - - {t("contracts.labels.insuranceexpired")} + + {t("contracts.labels.insuranceexpired")} - - ); - return <>>; - }} - - )} - - - - - {create && ( - - p.kmstart !== c.kmstart || p.scheduledreturn !== c.scheduledreturn - } - > - {() => { - const mileageOver = - selectedCar && selectedCar.nextservicekm - ? selectedCar.nextservicekm <= form.getFieldValue("kmstart") - : false; - const dueForService = - selectedCar && - selectedCar.nextservicedate && - dayjs(selectedCar.nextservicedate) - .endOf("day") - .isSameOrBefore( - dayjs(form.getFieldValue("scheduledreturn")) - ); - if (mileageOver || dueForService) - return ( - + + ); + return <>>; + }} + + )} + + + + + + {create && ( + p.kmstart !== c.kmstart || p.scheduledreturn !== c.scheduledreturn}> + {() => { + const mileageOver = + selectedCar && selectedCar.nextservicekm + ? selectedCar.nextservicekm <= form.getFieldValue("kmstart") + : false; + const dueForService = + selectedCar && + selectedCar.nextservicedate && + dayjs(selectedCar.nextservicedate) + .endOf("day") + .isSameOrBefore(dayjs(form.getFieldValue("scheduledreturn"))); + if (mileageOver || dueForService) + return ( + - - {t("contracts.labels.cardueforservice")} + + {t("contracts.labels.cardueforservice")} - {`${ - selectedCar && selectedCar.nextservicekm - } km`} - - - {selectedCar && selectedCar.nextservicedate} - + {`${selectedCar && selectedCar.nextservicekm} km`} + + {selectedCar && selectedCar.nextservicedate} - - ); + + ); - return <>>; - }} - - )} - {create ? null : ( - - - - )} - - - - - - - - - {create ? null : ( - - - - )} - + return <>>; + }} + + )} + {create ? null : ( + + + + )} + + + + + + + + + {create ? null : ( + + + + )} + + + + {selectedJobState && ( - - {selectedJobState && ( - - - - )} - { - // - } - + - + )} + { + // + } + + + + + + + p.driver_dlexpiry !== c.driver_dlexpiry || p.scheduledreturn !== c.scheduledreturn} + > + {() => { + const dlExpiresBeforeReturn = dayjs(form.getFieldValue("driver_dlexpiry")).isBefore( + dayjs(form.getFieldValue("scheduledreturn")) + ); + + return ( + - - - - p.driver_dlexpiry !== c.driver_dlexpiry || - p.scheduledreturn !== c.scheduledreturn + label={t("contracts.fields.driver_dlexpiry")} + name="driver_dlexpiry" + rules={[ + { + required: true + //message: t("general.validation.required"), } + ]} > - {() => { - const dlExpiresBeforeReturn = dayjs( - form.getFieldValue("driver_dlexpiry") - ).isBefore(dayjs(form.getFieldValue("scheduledreturn"))); + + + {dlExpiresBeforeReturn && ( + + + {t("contracts.labels.dlexpirebeforereturn")} + + )} + + ); + }} + - return ( - - - - - {dlExpiresBeforeReturn && ( - - - {t("contracts.labels.dlexpirebeforereturn")} - - )} - - ); - }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - PhoneItemFormatterValidation(getFieldValue, "driver_ph1"), - ]} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + + + + + + + + + + + + + + + + + + + + + + + + + PhoneItemFormatterValidation(getFieldValue, "driver_ph1") + ]} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/client/src/components/contract-job-block/contract-job-block.component.jsx b/client/src/components/contract-job-block/contract-job-block.component.jsx index e0d6af75b..526860b6b 100644 --- a/client/src/components/contract-job-block/contract-job-block.component.jsx +++ b/client/src/components/contract-job-block/contract-job-block.component.jsx @@ -1,33 +1,25 @@ -import {Card} from "antd"; +import { Card } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {Link} from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; import DataLabel from "../data-label/data-label.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -export default function ContractJobBlock({job}) { - const {t} = useTranslation(); - return ( - - - - - {(job && job.ro_number) || ""} - - - {`${(job && job.v_model_yr) || ""} ${ - (job && job.v_make_desc) || "" - } ${(job && job.v_model_desc) || ""}`} - - - - - - - - ); +export default function ContractJobBlock({ job }) { + const { t } = useTranslation(); + return ( + + + + {(job && job.ro_number) || ""} + + {`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) || ""} ${(job && job.v_model_desc) || ""}`} + + + + + + + + ); } diff --git a/client/src/components/contract-jobs/contract-jobs.component.jsx b/client/src/components/contract-jobs/contract-jobs.component.jsx index 84dfd6d4e..ea71c2538 100644 --- a/client/src/components/contract-jobs/contract-jobs.component.jsx +++ b/client/src/components/contract-jobs/contract-jobs.component.jsx @@ -1,201 +1,155 @@ -import {Card, Input, Table} from "antd"; -import React, {useMemo, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {alphaSort} from "../../utils/sorters"; +import { Card, Input, Table } from "antd"; +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { alphaSort } from "../../utils/sorters"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; -export default function ContractsJobsComponent({ - loading, - data, - selectedJob, - handleSelect, - }) { - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {text: ""}, - search: "", - }); +export default function ContractsJobsComponent({ loading, data, selectedJob, handleSelect }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" }, + search: "" + }); - const {t} = useTranslation(); + const { t } = useTranslation(); - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - width: "8%", - sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + width: "8%", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - - {record.ro_number ? record.ro_number : t("general.labels.na")} - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - width: "25%", - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => , - }, - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - width: "10%", - ellipsis: true, - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - render: (text, record) => { - return record.status || t("general.labels.na"); - }, - }, + render: (text, record) => {record.ro_number ? record.ro_number : t("general.labels.na")} + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + width: "25%", + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + width: "10%", + ellipsis: true, + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + render: (text, record) => { + return record.status || t("general.labels.na"); + } + }, - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - width: "15%", - ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - t("jobs.errors.novehicle") - ); - }, - }, - { - title: t("vehicles.fields.plate_no"), - dataIndex: "plate_no", - key: "plate_no", - width: "8%", - ellipsis: true, - sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), - sortOrder: - state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, - render: (text, record) => { - return record.plate_no ? ( - {record.plate_no} - ) : ( - t("general.labels.unknown") - ); - }, - }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - width: "12%", - ellipsis: true, - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: - state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, - render: (text, record) => { - return record.clm_no ? ( - {record.clm_no} - ) : ( - t("general.labels.unknown") - ); - }, - }, - ]; + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + width: "15%", + ellipsis: true, + render: (text, record) => { + return record.vehicleid ? ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ) : ( + t("jobs.errors.novehicle") + ); + } + }, + { + title: t("vehicles.fields.plate_no"), + dataIndex: "plate_no", + key: "plate_no", + width: "8%", + ellipsis: true, + sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), + sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, + render: (text, record) => { + return record.plate_no ? {record.plate_no} : t("general.labels.unknown"); + } + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + width: "12%", + ellipsis: true, + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), + sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, + render: (text, record) => { + return record.clm_no ? {record.clm_no} : t("general.labels.unknown"); + } + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const filteredData = - state.search === "" - ? data - : data.filter( - (j) => - (j.ro_number || "") - .toString() - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.ownr_co_nm || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.ownr_fn || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.ownr_ln || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.clm_no || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.v_make_desc || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.v_model_desc || "") - .toLowerCase() - .includes(state.search.toLowerCase()) || - (j.plate_no || "") - .toLowerCase() - .includes(state.search.toLowerCase()) - ); + const filteredData = + state.search === "" + ? data + : data.filter( + (j) => + (j.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) || + (j.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) || + (j.plate_no || "").toLowerCase().includes(state.search.toLowerCase()) + ); - const defaultCurrent = useMemo(() => { - const page = - Math.floor( - (filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10 - ) + 1; - if (page === 0) return 1; - return page; - }, [filteredData, selectedJob]); + const defaultCurrent = useMemo(() => { + const page = Math.floor((filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10) + 1; + if (page === 0) return 1; + return page; + }, [filteredData, selectedJob]); - if (loading) return ; - return ( - setState({...state, search: e.target.value})} - /> + if (loading) return ; + return ( + setState({ ...state, search: e.target.value })} + /> + } + > + { + return { + onClick: (event) => { + handleSelect(record); } - > - { - return { - onClick: (event) => { - handleSelect(record); - }, - }; - }} - /> - - ); + }; + }} + /> + + ); } diff --git a/client/src/components/contract-jobs/contract-jobs.container.jsx b/client/src/components/contract-jobs/contract-jobs.container.jsx index 926d89330..1db612a03 100644 --- a/client/src/components/contract-jobs/contract-jobs.container.jsx +++ b/client/src/components/contract-jobs/contract-jobs.container.jsx @@ -1,41 +1,41 @@ -import {useQuery} from "@apollo/client"; +import { useQuery } from "@apollo/client"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {QUERY_ALL_ACTIVE_JOBS} from "../../graphql/jobs.queries"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import ContractJobsComponent from "./contract-jobs.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); -export function ContractJobsContainer({selectedJobState, bodyshop}) { - const {loading, error, data} = useQuery(QUERY_ALL_ACTIVE_JOBS, { - variables: { - statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"], - isConverted: true, - }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); - const [selectedJob, setSelectedJob] = selectedJobState; +export function ContractJobsContainer({ selectedJobState, bodyshop }) { + const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, { + variables: { + statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"], + isConverted: true + }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + const [selectedJob, setSelectedJob] = selectedJobState; - const handleSelect = (record) => { - setSelectedJob(record.id); - }; + const handleSelect = (record) => { + setSelectedJob(record.id); + }; - if (error) return ; - return ( - - ); + if (error) return ; + return ( + + ); } export default connect(mapStateToProps, null)(ContractJobsContainer); diff --git a/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx b/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx index 49da3a5b2..f6bbf7e02 100644 --- a/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx +++ b/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx @@ -1,123 +1,101 @@ -import {Button, Input, Modal, Typography} from "antd"; +import { Button, Input, Modal, Typography } from "antd"; import dayjs from "../../utils/day"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import aamva from "../../utils/aamva"; import DataLabel from "../data-label/data-label.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; -import {logImEXEvent} from "../../firebase/firebase.utils"; +import { logImEXEvent } from "../../firebase/firebase.utils"; -export default function ContractLicenseDecodeButton({form}) { - const {t} = useTranslation(); - const [modalVisible, setModalVisible] = useState(false); - const [loading, setLoading] = useState(false); - const [decodedBarcode, setDecodedBarcode] = useState(null); +export default function ContractLicenseDecodeButton({ form }) { + const { t } = useTranslation(); + const [modalVisible, setModalVisible] = useState(false); + const [loading, setLoading] = useState(false); + const [decodedBarcode, setDecodedBarcode] = useState(null); - const handleDecode = (e) => { - logImEXEvent("contract_license_decode"); - setLoading(true); - const aamvaParse = aamva.parse(e.currentTarget.value); - setDecodedBarcode(aamvaParse); - setLoading(false); + const handleDecode = (e) => { + logImEXEvent("contract_license_decode"); + setLoading(true); + const aamvaParse = aamva.parse(e.currentTarget.value); + setDecodedBarcode(aamvaParse); + setLoading(false); + }; + + const handleInsertForm = () => { + logImEXEvent("contract_license_decode_fill_form"); + + const values = { + driver_dlnumber: decodedBarcode.dl, + driver_dlexpiry: dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`), + driver_dlst: decodedBarcode.state, + driver_fn: decodedBarcode.name.first, + driver_ln: decodedBarcode.name.last, + driver_addr1: decodedBarcode.address, + driver_city: decodedBarcode.city, + driver_state: decodedBarcode.state, + driver_zip: decodedBarcode.postal_code, + driver_dob: dayjs(decodedBarcode.birthday) }; - const handleInsertForm = () => { - logImEXEvent("contract_license_decode_fill_form"); + form.setFieldsValue(values); + setModalVisible(false); + setDecodedBarcode(null); + }; + const handleClick = () => { + setModalVisible(true); + }; + const handleCancel = () => { + setModalVisible(false); + }; - const values = { - driver_dlnumber: decodedBarcode.dl, - driver_dlexpiry: dayjs( - `20${decodedBarcode.expiration_date}${dayjs( - decodedBarcode.birthday - ).format("DD")}` - ), - driver_dlst: decodedBarcode.state, - driver_fn: decodedBarcode.name.first, - driver_ln: decodedBarcode.name.last, - driver_addr1: decodedBarcode.address, - driver_city: decodedBarcode.city, - driver_state: decodedBarcode.state, - driver_zip: decodedBarcode.postal_code, - driver_dob: dayjs(decodedBarcode.birthday), - }; - - form.setFieldsValue(values); - setModalVisible(false); - setDecodedBarcode(null); - }; - const handleClick = () => { - setModalVisible(true); - }; - const handleCancel = () => { - setModalVisible(false); - }; - - return ( + return ( + + - + + { + if (!loading) setLoading(true); + }} + onPressEnter={handleDecode} + /> + + + {decodedBarcode ? ( + + {decodedBarcode.state} + {decodedBarcode.dl} + {decodedBarcode.name.first} + {decodedBarcode.name.last} + {decodedBarcode.address} + {decodedBarcode.address} + + {dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`).format( + "MM/DD/YYYY" + )} + + + {dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")} + - - { - if (!loading) setLoading(true); - }} - onPressEnter={handleDecode} - /> - - - {decodedBarcode ? ( - - - {decodedBarcode.state} - - - {decodedBarcode.dl} - - - {decodedBarcode.name.first} - - - {decodedBarcode.name.last} - - - {decodedBarcode.address} - - - {decodedBarcode.address} - - - {dayjs( - `20${decodedBarcode.expiration_date}${dayjs( - decodedBarcode.birthday - ).format("DD")}` - ).format("MM/DD/YYYY")} - - - {dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")} - - - - {t("contracts.labels.correctdataonform")} - - - - ) : ( - {t("contracts.labels.waitingforscan")} - )} - + {t("contracts.labels.correctdataonform")} - - - {t("contracts.actions.decodelicense")} - + + ) : ( + {t("contracts.labels.waitingforscan")} + )} + - ); + + {t("contracts.actions.decodelicense")} + + ); } diff --git a/client/src/components/contract-status-select/contract-status-select.component.jsx b/client/src/components/contract-status-select/contract-status-select.component.jsx index 72a8a2e5c..8bd048195 100644 --- a/client/src/components/contract-status-select/contract-status-select.component.jsx +++ b/client/src/components/contract-status-select/contract-status-select.component.jsx @@ -1,33 +1,31 @@ -import React, {forwardRef, useEffect, useState} from "react"; -import {Select} from "antd"; -import {useTranslation} from "react-i18next"; +import React, { forwardRef, useEffect, useState } from "react"; +import { Select } from "antd"; +import { useTranslation } from "react-i18next"; -const {Option} = Select; +const { Option } = Select; -const ContractStatusComponent = ({value, onChange}, ref) => { - const [option, setOption] = useState(value); - const {t} = useTranslation(); +const ContractStatusComponent = ({ value, onChange }, ref) => { + const [option, setOption] = useState(value); + const { t } = useTranslation(); - useEffect(() => { - if (value !== option && onChange) { - onChange(option); - } - }, [value, option, onChange]); + useEffect(() => { + if (value !== option && onChange) { + onChange(option); + } + }, [value, option, onChange]); - return ( - - {t("contracts.status.new")} - {t("contracts.status.out")} - - {t("contracts.status.returned")} - - - ); + return ( + + {t("contracts.status.new")} + {t("contracts.status.out")} + {t("contracts.status.returned")} + + ); }; export default forwardRef(ContractStatusComponent); diff --git a/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx b/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx index 4bd08369a..52a216e7d 100644 --- a/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx +++ b/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx @@ -1,37 +1,37 @@ -import {Form, Input} from "antd"; +import { Form, Input } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); export default connect(mapStateToProps, null)(PartsReceiveModalComponent); -export function PartsReceiveModalComponent({bodyshop, form}) { - const {t} = useTranslation(); +export function PartsReceiveModalComponent({ bodyshop, form }) { + const { t } = useTranslation(); - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); } diff --git a/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx b/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx index e7398f6a3..eb3ae6fd4 100644 --- a/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx +++ b/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx @@ -1,169 +1,143 @@ -import {useLazyQuery} from "@apollo/client"; -import {Button, Form, Modal, Table} from "antd"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {Link} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {FIND_CONTRACT} from "../../graphql/cccontracts.queries"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectContractFinder} from "../../redux/modals/modals.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; +import { useLazyQuery } from "@apollo/client"; +import { Button, Form, Modal, Table } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { FIND_CONTRACT } from "../../graphql/cccontracts.queries"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectContractFinder } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; import ContractsFindModalComponent from "./contracts-find-modal.component"; import AlertComponent from "../alert/alert.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - contractFinderModal: selectContractFinder, + bodyshop: selectBodyshop, + contractFinderModal: selectContractFinder }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder")), + toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder")) }); export function ContractsFindModalContainer({ - contractFinderModal, - toggleModalVisible, + contractFinderModal, + toggleModalVisible, - bodyshop, - }) { - const {t} = useTranslation(); + bodyshop +}) { + const { t } = useTranslation(); - const {open} = contractFinderModal; + const { open } = contractFinderModal; - const [form] = Form.useForm(); + const [form] = Form.useForm(); - // const [updateJobLines] = useMutation(UPDATE_JOB_LINE); - const [callSearch, {loading, error, data}] = useLazyQuery(FIND_CONTRACT); - const handleFinish = async (values) => { - logImEXEvent("contract_finder_search"); + // const [updateJobLines] = useMutation(UPDATE_JOB_LINE); + const [callSearch, { loading, error, data }] = useLazyQuery(FIND_CONTRACT); + const handleFinish = async (values) => { + logImEXEvent("contract_finder_search"); - //Execute contract find + //Execute contract find - callSearch({ - variables: { - plate: - (values.plate && values.plate !== "" && values.plate) || undefined, - time: values.time, + callSearch({ + variables: { + plate: (values.plate && values.plate !== "" && values.plate) || undefined, + time: values.time + } + }); + }; + + useEffect(() => { + if (open) { + form.resetFields(); + } + }, [open, form]); + + return ( + toggleModalVisible()} + onOk={() => toggleModalVisible()} + destroyOnClose + forceRender + > + + + form.submit()} type="primary" loading={loading}> + {t("general.labels.search")} + + {error && } + ( + {record.agreementnumber || ""} + ) }, - }); - }; - - useEffect(() => { - if (open) { - form.resetFields(); - } - }, [open, form]); - - return ( - toggleModalVisible()} - onOk={() => toggleModalVisible()} - destroyOnClose - forceRender - > - - - form.submit()} type="primary" loading={loading}> - {t("general.labels.search")} - - {error && ( - - )} - ( - - {record.agreementnumber || ""} - - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "job.ro_number", - key: "job.ro_number", - render: (text, record) => ( - - {record.job.ro_number || ""} - - ), - }, - { - title: t("contracts.fields.driver"), - dataIndex: "driver_ln", - key: "driver_ln", - render: (text, record) => - `${record.driver_fn || ""} ${record.driver_ln || ""}`, - }, - { - title: t("contracts.labels.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - render: (text, record) => ( - {`${ - record.courtesycar.year - } ${record.courtesycar.make} ${record.courtesycar.model} ${ - record.courtesycar.plate - ? `(${record.courtesycar.plate})` - : "" - }`} - ), - }, - { - title: t("contracts.fields.status"), - dataIndex: "status", - render: (text, record) => t(record.status), - }, - { - title: t("contracts.fields.start"), - dataIndex: "start", - key: "start", - render: (text, record) => ( - {record.start} - ), - }, - { - title: t("contracts.fields.scheduledreturn"), - dataIndex: "scheduledreturn", - key: "scheduledreturn", - render: (text, record) => ( - {record.scheduledreturn} - ), - }, - { - title: t("contracts.fields.actualreturn"), - dataIndex: "actualreturn", - key: "actualreturn", - render: (text, record) => ( - {record.actualreturn} - ), - }, - ]} - rowKey="id" - dataSource={data && data.cccontracts} - /> - - - ); + { + title: t("jobs.fields.ro_number"), + dataIndex: "job.ro_number", + key: "job.ro_number", + render: (text, record) => {record.job.ro_number || ""} + }, + { + title: t("contracts.fields.driver"), + dataIndex: "driver_ln", + key: "driver_ln", + render: (text, record) => `${record.driver_fn || ""} ${record.driver_ln || ""}` + }, + { + title: t("contracts.labels.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + render: (text, record) => ( + {`${ + record.courtesycar.year + } ${record.courtesycar.make} ${record.courtesycar.model} ${ + record.courtesycar.plate ? `(${record.courtesycar.plate})` : "" + }`} + ) + }, + { + title: t("contracts.fields.status"), + dataIndex: "status", + render: (text, record) => t(record.status) + }, + { + title: t("contracts.fields.start"), + dataIndex: "start", + key: "start", + render: (text, record) => {record.start} + }, + { + title: t("contracts.fields.scheduledreturn"), + dataIndex: "scheduledreturn", + key: "scheduledreturn", + render: (text, record) => {record.scheduledreturn} + }, + { + title: t("contracts.fields.actualreturn"), + dataIndex: "actualreturn", + key: "actualreturn", + render: (text, record) => {record.actualreturn} + } + ]} + rowKey="id" + dataSource={data && data.cccontracts} + /> + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ContractsFindModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer); diff --git a/client/src/components/contracts-list/contracts-list.component.jsx b/client/src/components/contracts-list/contracts-list.component.jsx index 4cd2a8a38..a42afe90a 100644 --- a/client/src/components/contracts-list/contracts-list.component.jsx +++ b/client/src/components/contracts-list/contracts-list.component.jsx @@ -1,224 +1,188 @@ -import {SyncOutlined} from "@ant-design/icons"; -import {Button, Card, Input, Space, Table, Typography} from "antd"; +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Input, Space, Table, Typography } from "antd"; import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Link, useLocation, useNavigate} from "react-router-dom"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; -import {alphaSort} from "../../utils/sorters"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { alphaSort } from "../../utils/sorters"; import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container"; import dayjs from "../../utils/day"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {pageLimit} from "../../utils/config"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) - setContractFinderContext: (context) => - dispatch(setModalContext({context: context, modal: "contractFinder"})), + //setUserLanguage: language => dispatch(setUserLanguage(language)) + setContractFinderContext: (context) => dispatch(setModalContext({ context: context, modal: "contractFinder" })) }); export default connect(mapStateToProps, mapDispatchToProps)(ContractsList); -export function ContractsList({ - bodyshop, - loading, - contracts, - refetch, - total, - setContractFinderContext, - }) { - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {text: ""}, - }); - const history = useNavigate(); - const search = queryString.parse(useLocation().search); - const {page} = search; +export function ContractsList({ bodyshop, loading, contracts, refetch, total, setContractFinderContext }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" } + }); + const history = useNavigate(); + const search = queryString.parse(useLocation().search); + const { page } = search; - const {t} = useTranslation(); + const { t } = useTranslation(); - const columns = [ - { - title: t("contracts.fields.agreementnumber"), - dataIndex: "agreementnumber", - key: "agreementnumber", - sorter: (a, b) => a.agreementnumber - b.agreementnumber, - sortOrder: - state.sortedInfo.columnKey === "agreementnumber" && - state.sortedInfo.order, - render: (text, record) => ( - - {record.agreementnumber || ""} - - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "job.ro_number", - key: "job.ro_number", - render: (text, record) => ( - - {record.job.ro_number || ""} - - ), - }, - { - title: t("contracts.fields.driver"), - dataIndex: "driver_ln", - key: "driver_ln", - sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln), - sortOrder: - state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order, - render: (text, record) => - bodyshop.last_name_first - ? `${record.driver_ln || ""}, ${record.driver_fn || ""}` - : `${record.driver_fn || ""} ${record.driver_ln || ""}`, - }, - { - title: t("contracts.labels.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - //sorter: (a, b) => alphaSort(a.status, b.status), - //sortOrder: - // state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - render: (text, record) => ( - {`${ - record.courtesycar.year - } ${record.courtesycar.make} ${record.courtesycar.model}${ - record.courtesycar.plate ? ` (${record.courtesycar.plate})` : "" - }${ - record.courtesycar.fleetnumber - ? ` (${record.courtesycar.fleetnumber})` - : "" - }`} - ), - }, - { - title: t("contracts.fields.status"), - dataIndex: "status", - key: "status", - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - render: (text, record) => t(record.status), - }, - { - title: t("contracts.fields.start"), - dataIndex: "start", - key: "start", - sorter: (a, b) => alphaSort(a.start, b.start), - sortOrder: - state.sortedInfo.columnKey === "start" && state.sortedInfo.order, - render: (text, record) => ( - {record.start} - ), - }, - { - title: t("contracts.fields.scheduledreturn"), - dataIndex: "scheduledreturn", - key: "scheduledreturn", - sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn), - sortOrder: - state.sortedInfo.columnKey === "scheduledreturn" && - state.sortedInfo.order, - render: (text, record) => ( - {record.scheduledreturn} - ), - }, - { - title: t("contracts.fields.actualreturn"), - dataIndex: "actualreturn", - key: "actualreturn", - sorter: (a, b) => alphaSort(a.actualreturn, b.actualreturn), - sortOrder: - state.sortedInfo.columnKey === "actualreturn" && state.sortedInfo.order, - render: (text, record) => ( - {record.actualreturn} - ), - }, - { - title: t("contracts.fields.length"), - dataIndex: "length", - key: "length", + const columns = [ + { + title: t("contracts.fields.agreementnumber"), + dataIndex: "agreementnumber", + key: "agreementnumber", + sorter: (a, b) => a.agreementnumber - b.agreementnumber, + sortOrder: state.sortedInfo.columnKey === "agreementnumber" && state.sortedInfo.order, + render: (text, record) => ( + {record.agreementnumber || ""} + ) + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "job.ro_number", + key: "job.ro_number", + render: (text, record) => {record.job.ro_number || ""} + }, + { + title: t("contracts.fields.driver"), + dataIndex: "driver_ln", + key: "driver_ln", + sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln), + sortOrder: state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order, + render: (text, record) => + bodyshop.last_name_first + ? `${record.driver_ln || ""}, ${record.driver_fn || ""}` + : `${record.driver_fn || ""} ${record.driver_ln || ""}` + }, + { + title: t("contracts.labels.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + //sorter: (a, b) => alphaSort(a.status, b.status), + //sortOrder: + // state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + render: (text, record) => ( + {`${ + record.courtesycar.year + } ${record.courtesycar.make} ${record.courtesycar.model}${ + record.courtesycar.plate ? ` (${record.courtesycar.plate})` : "" + }${record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""}`} + ) + }, + { + title: t("contracts.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + render: (text, record) => t(record.status) + }, + { + title: t("contracts.fields.start"), + dataIndex: "start", + key: "start", + sorter: (a, b) => alphaSort(a.start, b.start), + sortOrder: state.sortedInfo.columnKey === "start" && state.sortedInfo.order, + render: (text, record) => {record.start} + }, + { + title: t("contracts.fields.scheduledreturn"), + dataIndex: "scheduledreturn", + key: "scheduledreturn", + sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn), + sortOrder: state.sortedInfo.columnKey === "scheduledreturn" && state.sortedInfo.order, + render: (text, record) => {record.scheduledreturn} + }, + { + title: t("contracts.fields.actualreturn"), + dataIndex: "actualreturn", + key: "actualreturn", + sorter: (a, b) => alphaSort(a.actualreturn, b.actualreturn), + sortOrder: state.sortedInfo.columnKey === "actualreturn" && state.sortedInfo.order, + render: (text, record) => {record.actualreturn} + }, + { + title: t("contracts.fields.length"), + dataIndex: "length", + key: "length", - render: (text, record) => - (record.actualreturn && - record.start && - `${dayjs(record.actualreturn) - .diff(dayjs(record.start), "day", true) - .toFixed(1)} days`) || - "", - }, - ]; + render: (text, record) => + (record.actualreturn && + record.start && + `${dayjs(record.actualreturn).diff(dayjs(record.start), "day", true).toFixed(1)} days`) || + "" + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - search.page = pagination.current; - search.sortcolumn = sorter.columnKey; - search.sortorder = sorter.order; - history({search: queryString.stringify(search)}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + search.page = pagination.current; + search.sortcolumn = sorter.columnKey; + search.sortorder = sorter.order; + history({ search: queryString.stringify(search) }); + }; - return ( - - {search.search && ( - <> - - {t("general.labels.searchresults", {search: search.search})} - - { - delete search.search; - history({search: queryString.stringify(search)}); - }} - > - {t("general.actions.clear")} - - > - )} - setContractFinderContext()}> - {t("contracts.actions.find")} - - refetch()}> - - - { - search.search = value; - history({search: queryString.stringify(search)}); - }} - /> - - } - > - - + {search.search && ( + <> + + {t("general.labels.searchresults", { search: search.search })} + + { + delete search.search; + history({ search: queryString.stringify(search) }); }} - pagination={{ - position: "top", - pageSize: pageLimit, - current: parseInt(page || 1), - total: total, - }} - columns={columns} - rowKey="id" - dataSource={contracts} - onChange={handleTableChange} - /> - - ); + > + {t("general.actions.clear")} + + > + )} + setContractFinderContext()}>{t("contracts.actions.find")} + refetch()}> + + + { + search.search = value; + history({ search: queryString.stringify(search) }); + }} + /> + + } + > + + + + ); } diff --git a/client/src/components/contracts-rates-change-button/contracts-rates-change-button.component.jsx b/client/src/components/contracts-rates-change-button/contracts-rates-change-button.component.jsx index 0b8508255..922636804 100644 --- a/client/src/components/contracts-rates-change-button/contracts-rates-change-button.component.jsx +++ b/client/src/components/contracts-rates-change-button/contracts-rates-change-button.component.jsx @@ -1,42 +1,38 @@ -import {DownOutlined} from "@ant-design/icons"; -import {Dropdown} from "antd"; +import { DownOutlined } from "@ant-design/icons"; +import { Dropdown } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); -export function ContractsRatesChangeButton({disabled, form, bodyshop}) { - const {t} = useTranslation(); +export function ContractsRatesChangeButton({ disabled, form, bodyshop }) { + const { t } = useTranslation(); - const handleClick = ({item, key, keyPath}) => { - const {label, ...rate} = item.props.value; - form.setFieldsValue(rate); - }; + const handleClick = ({ item, key, keyPath }) => { + const { label, ...rate } = item.props.value; + form.setFieldsValue(rate); + }; - const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({ - key: idx, - label: i.label, - value: i, - })); + const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({ + key: idx, + label: i.label, + value: i + })); - const menu = {items: menuItems, onClick: handleClick}; + const menu = { items: menuItems, onClick: handleClick }; - return ( - - e.preventDefault()} - > - {t("contracts.actions.changerate")} - - - ); + return ( + + e.preventDefault()}> + {t("contracts.actions.changerate")} + + + ); } export default connect(mapStateToProps, null)(ContractsRatesChangeButton); diff --git a/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx b/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx index d4b6f846c..34c6e3bfc 100644 --- a/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx +++ b/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx @@ -1,104 +1,92 @@ -import {Card, Table} from "antd"; +import { Card, Table } from "antd"; import queryString from "query-string"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {Link, useLocation, useNavigate} from "react-router-dom"; -import {DateFormatter} from "../../utils/DateFormatter"; -import {alphaSort} from "../../utils/sorters"; -import {pageLimit} from "../../utils/config"; +import { useTranslation } from "react-i18next"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { DateFormatter } from "../../utils/DateFormatter"; +import { alphaSort } from "../../utils/sorters"; +import { pageLimit } from "../../utils/config"; -export default function CourtesyCarContractListComponent({ - contracts, - totalContracts, - }) { - const search = queryString.parse(useLocation().search); - const {page, sortcolumn, sortorder} = search; - const history = useNavigate(); +export default function CourtesyCarContractListComponent({ contracts, totalContracts }) { + const search = queryString.parse(useLocation().search); + const { page, sortcolumn, sortorder } = search; + const history = useNavigate(); - const {t} = useTranslation(); + const { t } = useTranslation(); - const columns = [ - { - title: t("contracts.fields.agreementnumber"), - dataIndex: "agreementnumber", - key: "agreementnumber", - sorter: (a, b) => a.agreementnumber - b.agreementnumber, - sortOrder: sortcolumn === "agreementnumber" && sortorder, - render: (text, record) => ( - - {record.agreementnumber || ""} - - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "job.ro_number", - key: "job.ro_number", + const columns = [ + { + title: t("contracts.fields.agreementnumber"), + dataIndex: "agreementnumber", + key: "agreementnumber", + sorter: (a, b) => a.agreementnumber - b.agreementnumber, + sortOrder: sortcolumn === "agreementnumber" && sortorder, + render: (text, record) => ( + {record.agreementnumber || ""} + ) + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "job.ro_number", + key: "job.ro_number", - render: (text, record) => ( - - {record.job.ro_number || ""} - - ), - }, - { - title: t("contracts.fields.driver"), - dataIndex: "driver_ln", - key: "driver_ln", + render: (text, record) => {record.job.ro_number || ""} + }, + { + title: t("contracts.fields.driver"), + dataIndex: "driver_ln", + key: "driver_ln", - render: (text, record) => - `${record.driver_fn || ""} ${record.driver_ln || ""}`, - }, - { - title: t("contracts.fields.status"), - dataIndex: "status", - key: "status", - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: sortcolumn === "status" && sortorder, - render: (text, record) => t(record.status), - }, - { - title: t("contracts.fields.start"), - dataIndex: "start", - key: "start", - sorter: (a, b) => alphaSort(a.start, b.start), - sortOrder: sortcolumn === "start" && sortorder, - render: (text, record) => {record.start}, - }, - { - title: t("contracts.fields.scheduledreturn"), - dataIndex: "scheduledreturn", - key: "scheduledreturn", - sorter: (a, b) => a.scheduledreturn - b.scheduledreturn, - sortOrder: sortcolumn === "scheduledreturn" && sortorder, - render: (text, record) => ( - {record.scheduledreturn} - ), - }, - ]; + render: (text, record) => `${record.driver_fn || ""} ${record.driver_ln || ""}` + }, + { + title: t("contracts.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: sortcolumn === "status" && sortorder, + render: (text, record) => t(record.status) + }, + { + title: t("contracts.fields.start"), + dataIndex: "start", + key: "start", + sorter: (a, b) => alphaSort(a.start, b.start), + sortOrder: sortcolumn === "start" && sortorder, + render: (text, record) => {record.start} + }, + { + title: t("contracts.fields.scheduledreturn"), + dataIndex: "scheduledreturn", + key: "scheduledreturn", + sorter: (a, b) => a.scheduledreturn - b.scheduledreturn, + sortOrder: sortcolumn === "scheduledreturn" && sortorder, + render: (text, record) => {record.scheduledreturn} + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - search.page = pagination.current; - search.sortcolumn = sorter.columnKey; - search.sortorder = sorter.order; - history({search: queryString.stringify(search)}); - }; + const handleTableChange = (pagination, filters, sorter) => { + search.page = pagination.current; + search.sortcolumn = sorter.columnKey; + search.sortorder = sorter.order; + history({ search: queryString.stringify(search) }); + }; - return ( - - - - ); + return ( + + + + ); } diff --git a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx index c738a87fb..b08e53db6 100644 --- a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx +++ b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx @@ -1,12 +1,12 @@ -import {WarningFilled} from "@ant-design/icons"; -import {useApolloClient} from "@apollo/client"; -import {Button, Form, Input, InputNumber, Space} from "antd"; -import {PageHeader} from "@ant-design/pro-layout"; +import { WarningFilled } from "@ant-design/icons"; +import { useApolloClient } from "@apollo/client"; +import { Button, Form, Input, InputNumber, Space } from "antd"; +import { PageHeader } from "@ant-design/pro-layout"; import dayjs from "../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {CHECK_CC_FLEET_NUMBER} from "../../graphql/courtesy-car.queries"; -import {DateFormatter} from "../../utils/DateFormatter"; +import { useTranslation } from "react-i18next"; +import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries"; +import { DateFormatter } from "../../utils/DateFormatter"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; @@ -15,352 +15,306 @@ import FormDatePicker from "../form-date-picker/form-date-picker.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -export default function CourtesyCarCreateFormComponent({form, saveLoading}) { - const {t} = useTranslation(); - const client = useApolloClient(); +export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { + const { t } = useTranslation(); + const client = useApolloClient(); - return ( - - form.submit()} - > - {t("general.actions.save")} - + return ( + + form.submit()}> + {t("general.actions.save")} + + } + /> + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + ({ + async validator(rule, value) { + if (value) { + const response = await client.query({ + query: CHECK_CC_FLEET_NUMBER, + variables: { + name: value + } + }); + + if (response.data.courtesycars_aggregate.aggregate.count === 0) { + return Promise.resolve(); + } else if ( + response.data.courtesycars_aggregate.nodes.length === 1 && + response.data.courtesycars_aggregate.nodes[0].id === form.getFieldValue("id") + ) { + return Promise.resolve(); + } + return Promise.reject(t("courtesycars.labels.uniquefleet")); + } else { + return Promise.resolve(); } - /> + } + }) + ]} + > + + + + + + + + + + + + + + + - {/* */} - - - - - - - - - - - - - - - - - - - - - - - - - ({ - async validator(rule, value) { - if (value) { - const response = await client.query({ - query: CHECK_CC_FLEET_NUMBER, - variables: { - name: value, - }, - }); - - if ( - response.data.courtesycars_aggregate.aggregate.count === 0 - ) { - return Promise.resolve(); - } else if ( - response.data.courtesycars_aggregate.nodes.length === 1 && - response.data.courtesycars_aggregate.nodes[0].id === - form.getFieldValue("id") - ) { - return Promise.resolve(); - } - return Promise.reject(t("courtesycars.labels.uniquefleet")); - } else { - return Promise.resolve(); - } - }, - }), - ]} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm - } - > - {() => { - const nextservicekm = form.getFieldValue("nextservicekm"); - const mileageOver = nextservicekm - ? nextservicekm <= form.getFieldValue("mileage") - : false; - if (mileageOver) - return ( - + + + + + + + + + + + + + + + p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm}> + {() => { + const nextservicekm = form.getFieldValue("nextservicekm"); + const mileageOver = nextservicekm ? nextservicekm <= form.getFieldValue("mileage") : false; + if (mileageOver) + return ( + - - {t("contracts.labels.cardueforservice")} + + {t("contracts.labels.cardueforservice")} - {`${nextservicekm} km`} - - ); + {`${nextservicekm} km`} + + ); - return <>>; - }} - - - - - - - p.nextservicedate !== c.nextservicedate} - > - {() => { - const nextservicedate = form.getFieldValue("nextservicedate"); - const dueForService = - nextservicedate && - dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs()); + return <>>; + }} + + + + + + + p.nextservicedate !== c.nextservicedate}> + {() => { + const nextservicedate = form.getFieldValue("nextservicedate"); + const dueForService = nextservicedate && dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs()); - if (dueForService) - return ( - + if (dueForService) + return ( + - - {t("contracts.labels.cardueforservice")} + + {t("contracts.labels.cardueforservice")} - + {nextservicedate} - - ); + + ); - return <>>; - }} - - - - - - - - - - - - - - p.registrationexpires !== c.registrationexpires - } - > - {() => { - const expires = form.getFieldValue("registrationexpires"); - - const dateover = - expires && dayjs(expires).endOf("day").isBefore(dayjs()); - - if (dateover) - return ( - - - - {t("contracts.labels.dateinpast")} - - - ); - - return <>>; - }} - - - - - - - p.insuranceexpires !== c.insuranceexpires} - > - {() => { - const expires = form.getFieldValue("insuranceexpires"); - - const dateover = - expires && dayjs(expires).endOf("day").isBefore(dayjs()); - - if (dateover) - return ( - - - - {t("contracts.labels.dateinpast")} - - - ); - - return <>>; - }} - - - - - - + return <>>; + }} + - ); + + + + + + + + + + + p.registrationexpires !== c.registrationexpires}> + {() => { + const expires = form.getFieldValue("registrationexpires"); + + const dateover = expires && dayjs(expires).endOf("day").isBefore(dayjs()); + + if (dateover) + return ( + + + + {t("contracts.labels.dateinpast")} + + + ); + + return <>>; + }} + + + + + + + p.insuranceexpires !== c.insuranceexpires}> + {() => { + const expires = form.getFieldValue("insuranceexpires"); + + const dateover = expires && dayjs(expires).endOf("day").isBefore(dayjs()); + + if (dateover) + return ( + + + + {t("contracts.labels.dateinpast")} + + + ); + + return <>>; + }} + + + + + + + + ); } diff --git a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx index a76193568..ffe1c1f97 100644 --- a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx +++ b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx @@ -1,66 +1,66 @@ -import {Slider} from "antd"; -import React, {forwardRef} from "react"; -import {useTranslation} from "react-i18next"; +import { Slider } from "antd"; +import React, { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; const CourtesyCarFuelComponent = (props, ref) => { - const {t} = useTranslation(); + const { t } = useTranslation(); - const marks = { - 0: { - style: { - color: "#f50", - }, - label: {t("courtesycars.labels.fuel.empty")}, - }, - 13: t("courtesycars.labels.fuel.18"), - 25: t("courtesycars.labels.fuel.14"), - 38: t("courtesycars.labels.fuel.38"), - 50: t("courtesycars.labels.fuel.12"), - 63: t("courtesycars.labels.fuel.58"), - 75: t("courtesycars.labels.fuel.34"), - 88: t("courtesycars.labels.fuel.78"), - 100: { - style: { - color: "#008000", - }, - label: {t("courtesycars.labels.fuel.full")}, - }, - }; + const marks = { + 0: { + style: { + color: "#f50" + }, + label: {t("courtesycars.labels.fuel.empty")} + }, + 13: t("courtesycars.labels.fuel.18"), + 25: t("courtesycars.labels.fuel.14"), + 38: t("courtesycars.labels.fuel.38"), + 50: t("courtesycars.labels.fuel.12"), + 63: t("courtesycars.labels.fuel.58"), + 75: t("courtesycars.labels.fuel.34"), + 88: t("courtesycars.labels.fuel.78"), + 100: { + style: { + color: "#008000" + }, + label: {t("courtesycars.labels.fuel.full")} + } + }; - return ( - { - switch (value) { - case 0: - return t("courtesycars.labels.fuel.empty"); - case 13: - return t("courtesycars.labels.fuel.18"); - case 25: - return t("courtesycars.labels.fuel.14"); - case 38: - return t("courtesycars.labels.fuel.38"); - case 50: - return t("courtesycars.labels.fuel.12"); - case 63: - return t("courtesycars.labels.fuel.58"); - case 75: - return t("courtesycars.labels.fuel.34"); - case 88: - return t("courtesycars.labels.fuel.78"); - case 100: - return t("courtesycars.labels.fuel.full"); - default: - return value; - } - }, - }} - /> - ); + return ( + { + switch (value) { + case 0: + return t("courtesycars.labels.fuel.empty"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 38: + return t("courtesycars.labels.fuel.38"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 63: + return t("courtesycars.labels.fuel.58"); + case 75: + return t("courtesycars.labels.fuel.34"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 100: + return t("courtesycars.labels.fuel.full"); + default: + return value; + } + } + }} + /> + ); }; export default forwardRef(CourtesyCarFuelComponent); diff --git a/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx index b33cf4539..553798328 100644 --- a/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx +++ b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx @@ -1,36 +1,32 @@ -import {Select} from "antd"; -import React, {forwardRef, useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; +import { Select } from "antd"; +import React, { forwardRef, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; -const {Option} = Select; +const { Option } = Select; -const CourtesyCarReadinessComponent = ({value, onChange}, ref) => { - const [option, setOption] = useState(value); - const {t} = useTranslation(); +const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => { + const [option, setOption] = useState(value); + const { t } = useTranslation(); - useEffect(() => { - if (value !== option && onChange) { - onChange(option); - } - }, [value, option, onChange]); + useEffect(() => { + if (value !== option && onChange) { + onChange(option); + } + }, [value, option, onChange]); - return ( - - - {t("courtesycars.readiness.ready")} - - - {t("courtesycars.readiness.notready")} - - - ); + return ( + + {t("courtesycars.readiness.ready")} + {t("courtesycars.readiness.notready")} + + ); }; export default forwardRef(CourtesyCarReadinessComponent); diff --git a/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.component.jsx b/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.component.jsx index f92302d3c..589c97ad4 100644 --- a/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.component.jsx +++ b/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.component.jsx @@ -1,50 +1,50 @@ -import {Form, InputNumber} from "antd"; +import { Form, InputNumber } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; export default function CourtesyCarReturnModalComponent() { - const {t} = useTranslation(); + const { t } = useTranslation(); - return ( - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + ); } diff --git a/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.container.jsx b/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.container.jsx index 04fe0d126..acbfc6ff0 100644 --- a/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.container.jsx +++ b/client/src/components/courtesy-car-return-modal/courtesy-car-return-modal.container.jsx @@ -1,88 +1,77 @@ -import {Form, Modal, notification} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectCourtesyCarReturn} from "../../redux/modals/modals.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { Form, Modal, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component"; import dayjs from "../../utils/day"; -import {RETURN_CONTRACT} from "../../graphql/cccontracts.queries"; -import {useMutation} from "@apollo/client"; +import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries"; +import { useMutation } from "@apollo/client"; const mapStateToProps = createStructuredSelector({ - courtesyCarReturnModal: selectCourtesyCarReturn, - bodyshop: selectBodyshop, + courtesyCarReturnModal: selectCourtesyCarReturn, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn")), + toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn")) }); -export function CCReturnModalContainer({ - courtesyCarReturnModal, - toggleModalVisible, - bodyshop, - }) { - const [loading, setLoading] = useState(false); - const {open, context, actions} = courtesyCarReturnModal; - const {t} = useTranslation(); - const [form] = Form.useForm(); - const [updateContract] = useMutation(RETURN_CONTRACT); - const handleFinish = (values) => { - setLoading(true); - updateContract({ - variables: { - contractId: context.contractId, - cccontract: { - kmend: values.kmend, - actualreturn: values.actualreturn, - status: "contracts.status.returned", - fuelin: values.fuelin, - }, - courtesycarid: context.courtesyCarId, - courtesycar: { - status: "courtesycars.status.in", - fuel: values.fuelin, - mileage: values.kmend, - }, - }, - }) - .then((r) => { - if (actions.refetch) actions.refetch(); - toggleModalVisible(); - }) - .catch((error) => { - notification["error"]({ - message: t("contracts.errors.returning", {error: error}), - }); - }); - setLoading(false); - }; +export function CCReturnModalContainer({ courtesyCarReturnModal, toggleModalVisible, bodyshop }) { + const [loading, setLoading] = useState(false); + const { open, context, actions } = courtesyCarReturnModal; + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [updateContract] = useMutation(RETURN_CONTRACT); + const handleFinish = (values) => { + setLoading(true); + updateContract({ + variables: { + contractId: context.contractId, + cccontract: { + kmend: values.kmend, + actualreturn: values.actualreturn, + status: "contracts.status.returned", + fuelin: values.fuelin + }, + courtesycarid: context.courtesyCarId, + courtesycar: { + status: "courtesycars.status.in", + fuel: values.fuelin, + mileage: values.kmend + } + } + }) + .then((r) => { + if (actions.refetch) actions.refetch(); + toggleModalVisible(); + }) + .catch((error) => { + notification["error"]({ + message: t("contracts.errors.returning", { error: error }) + }); + }); + setLoading(false); + }; - return ( - toggleModalVisible()} - width={"90%"} - okText={t("general.actions.save")} - onOk={() => form.submit()} - okButtonProps={{htmlType: "submit", loading: loading}} - > - - - - - ); + return ( + toggleModalVisible()} + width={"90%"} + okText={t("general.actions.save")} + onOk={() => form.submit()} + okButtonProps={{ htmlType: "submit", loading: loading }} + > + + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(CCReturnModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(CCReturnModalContainer); diff --git a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx index 64c08d643..72c886da5 100644 --- a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx +++ b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx @@ -1,44 +1,34 @@ -import React, {forwardRef, useEffect, useState} from "react"; -import {Select} from "antd"; -import {useTranslation} from "react-i18next"; +import React, { forwardRef, useEffect, useState } from "react"; +import { Select } from "antd"; +import { useTranslation } from "react-i18next"; -const {Option} = Select; +const { Option } = Select; -const CourtesyCarStatusComponent = ({value, onChange}, ref) => { - const [option, setOption] = useState(value); - const {t} = useTranslation(); +const CourtesyCarStatusComponent = ({ value, onChange }, ref) => { + const [option, setOption] = useState(value); + const { t } = useTranslation(); - useEffect(() => { - if (value !== option && onChange) { - onChange(option); - } - }, [value, option, onChange]); + useEffect(() => { + if (value !== option && onChange) { + onChange(option); + } + }, [value, option, onChange]); - return ( - - - {t("courtesycars.status.in")} - - - {t("courtesycars.status.inservice")} - - - {t("courtesycars.status.out")} - - - {t("courtesycars.status.sold")} - - - {t("courtesycars.status.leasereturn")} - - - ); + return ( + + {t("courtesycars.status.in")} + {t("courtesycars.status.inservice")} + {t("courtesycars.status.out")} + {t("courtesycars.status.sold")} + {t("courtesycars.status.leasereturn")} + + ); }; export default forwardRef(CourtesyCarStatusComponent); diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index 2409f9fdd..bf144de4a 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -1,13 +1,5 @@ import { SyncOutlined, WarningFilled } from "@ant-design/icons"; -import { - Button, - Card, - Dropdown, - Input, - Space, - Table, - Tooltip, -} from "antd"; +import { Button, Card, Dropdown, Input, Space, Table, Tooltip } from "antd"; import dayjs from "../../utils/day"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -20,297 +12,270 @@ import useLocalStorage from "../../utils/useLocalStorage"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; export default function CourtesyCarsList({ loading, courtesycars, refetch }) { - const [state, setState] = useState({ - sortedInfo: {}, - }); - const [searchText, setSearchText] = useState(""); - const [filter, setFilter] = useLocalStorage( - "filter_courtesy_cars_list", - null - ); - const { t } = useTranslation(); + const [state, setState] = useState({ + sortedInfo: {} + }); + const [searchText, setSearchText] = useState(""); + const [filter, setFilter] = useLocalStorage("filter_courtesy_cars_list", null); + const { t } = useTranslation(); - const columns = [ + const columns = [ + { + title: t("courtesycars.fields.fleetnumber"), + dataIndex: "fleetnumber", + key: "fleetnumber", + sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber), + sortOrder: state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.vin"), + dataIndex: "vin", + key: "vin", + sorter: (a, b) => alphaSort(a.vin, b.vin), + sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order, + render: (text, record) => {record.vin} + }, + { + title: t("courtesycars.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + filteredValue: filter?.status || null, + filters: [ { - title: t("courtesycars.fields.fleetnumber"), - dataIndex: "fleetnumber", - key: "fleetnumber", - sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber), - sortOrder: - state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order, + text: t("courtesycars.status.in"), + value: "courtesycars.status.in" }, { - title: t("courtesycars.fields.vin"), - dataIndex: "vin", - key: "vin", - sorter: (a, b) => alphaSort(a.vin, b.vin), - sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order, - render: (text, record) => ( - {record.vin} - ), - }, - { - title: t("courtesycars.fields.status"), - dataIndex: "status", - key: "status", - sorter: (a, b) => alphaSort(a.status, b.status), - filteredValue: filter?.status || null, - filters: [ - { - text: t("courtesycars.status.in"), - value: "courtesycars.status.in", - }, - { text: t("courtesycars.status.inservice"), - value: "courtesycars.status.inservice", + value: "courtesycars.status.inservice" }, { - text: t("courtesycars.status.out"), - value: "courtesycars.status.out", - }, - { - text: t("courtesycars.status.sold"), - value: "courtesycars.status.sold", - }, - { - text: t("courtesycars.status.leasereturn"), - value: "courtesycars.status.leasereturn", - }, - ], - onFilter: (value, record) => record.status === value, - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - render: (text, record) => { - const { nextservicedate, nextservicekm, mileage, insuranceexpires } = - record; + text: t("courtesycars.status.out"), + value: "courtesycars.status.out" + }, + { + text: t("courtesycars.status.sold"), + value: "courtesycars.status.sold" + }, + { + text: t("courtesycars.status.leasereturn"), + value: "courtesycars.status.leasereturn" + } + ], + onFilter: (value, record) => record.status === value, + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + render: (text, record) => { + const { nextservicedate, nextservicekm, mileage, insuranceexpires } = record; - const mileageOver = nextservicekm ? nextservicekm <= mileage : false; + const mileageOver = nextservicekm ? nextservicekm <= mileage : false; - const dueForService = - nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs()); + const dueForService = nextservicedate && dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs()); - const insuranceOver = - insuranceexpires && - dayjs(insuranceexpires).endOf("day").isBefore(dayjs()); + const insuranceOver = insuranceexpires && dayjs(insuranceexpires).endOf("day").isBefore(dayjs()); - return ( - - {t(record.status)} - {(mileageOver || dueForService || insuranceOver) && ( - - - - )} - - ); - }, + return ( + + {t(record.status)} + {(mileageOver || dueForService || insuranceOver) && ( + + + + )} + + ); + } + }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + filteredValue: filter?.readiness || null, + filters: [ + { + text: t("courtesycars.readiness.ready"), + value: "courtesycars.readiness.ready" }, { - title: t("courtesycars.fields.readiness"), - dataIndex: "readiness", - key: "readiness", - sorter: (a, b) => alphaSort(a.readiness, b.readiness), - filteredValue: filter?.readiness || null, - filters: [ - { - text: t("courtesycars.readiness.ready"), - value: "courtesycars.readiness.ready", - }, - { - text: t("courtesycars.readiness.notready"), - value: "courtesycars.readiness.notready", - }, - ], - onFilter: (value, record) => value.includes(record.readiness), - sortOrder: - state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, - render: (text, record) => t(record.readiness), - }, - { - title: t("courtesycars.fields.year"), - dataIndex: "year", - key: "year", - sorter: (a, b) => alphaSort(a.year, b.year), - sortOrder: - state.sortedInfo.columnKey === "year" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.make"), - dataIndex: "make", - key: "make", - sorter: (a, b) => alphaSort(a.make, b.make), - sortOrder: - state.sortedInfo.columnKey === "make" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.model"), - dataIndex: "model", - key: "model", - sorter: (a, b) => alphaSort(a.model, b.model), - sortOrder: - state.sortedInfo.columnKey === "model" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.color"), - dataIndex: "color", - key: "color", - sorter: (a, b) => alphaSort(a.color, b.color), - sortOrder: - state.sortedInfo.columnKey === "color" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.plate"), - dataIndex: "plate", - key: "plate", - sorter: (a, b) => alphaSort(a.plate, b.plate), - sortOrder: - state.sortedInfo.columnKey === "plate" && state.sortedInfo.order, - }, - { - title: t("courtesycars.fields.fuel"), - dataIndex: "fuel", - key: "fuel", - sorter: (a, b) => a.fuel - b.fuel, - sortOrder: - state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order, - render: (text, record) => { - switch (record.fuel) { - case 100: - return t("courtesycars.labels.fuel.full"); - case 88: - return t("courtesycars.labels.fuel.78"); - case 75: + text: t("courtesycars.readiness.notready"), + value: "courtesycars.readiness.notready" + } + ], + onFilter: (value, record) => value.includes(record.readiness), + sortOrder: state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness) + }, + { + title: t("courtesycars.fields.year"), + dataIndex: "year", + key: "year", + sorter: (a, b) => alphaSort(a.year, b.year), + sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.make"), + dataIndex: "make", + key: "make", + sorter: (a, b) => alphaSort(a.make, b.make), + sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.model"), + dataIndex: "model", + key: "model", + sorter: (a, b) => alphaSort(a.model, b.model), + sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.color"), + dataIndex: "color", + key: "color", + sorter: (a, b) => alphaSort(a.color, b.color), + sortOrder: state.sortedInfo.columnKey === "color" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.plate"), + dataIndex: "plate", + key: "plate", + sorter: (a, b) => alphaSort(a.plate, b.plate), + sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order + }, + { + title: t("courtesycars.fields.fuel"), + dataIndex: "fuel", + key: "fuel", + sorter: (a, b) => a.fuel - b.fuel, + sortOrder: state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order, + render: (text, record) => { + switch (record.fuel) { + case 100: + return t("courtesycars.labels.fuel.full"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 75: return t("courtesycars.labels.fuel.34"); case 63: - return t("courtesycars.labels.fuel.58"); - case 50: - return t("courtesycars.labels.fuel.12"); - case 38: - return t("courtesycars.labels.fuel.38"); - case 25: - return t("courtesycars.labels.fuel.14"); - case 13: - return t("courtesycars.labels.fuel.18"); - case 0: - return t("courtesycars.labels.fuel.empty"); - default: - return record.fuel; - } - }, - }, - { - title: t("courtesycars.labels.outwith"), - dataIndex: "outwith", - key: "outwith", - // sorter: (a, b) => alphaSort(a.model, b.model), - sortOrder: - state.sortedInfo.columnKey === "model" && state.sortedInfo.order, - render: (text, record) => - record.cccontracts.length === 1 ? ( - - {`${ - record.cccontracts[0].job.ro_number - } - ${OwnerNameDisplayFunction(record.cccontracts[0].job)}`} - - ) : null, - }, - { - title: t("contracts.fields.scheduledreturn"), - dataIndex: "scheduledreturn", - key: "scheduledreturn", - render: (text, record) => - record.cccontracts.length === 1 && ( - - {record.cccontracts[0].scheduledreturn} - - ), - }, - ]; - - const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, sortedInfo: sorter }); - setFilter(filters); - }; - - const tableData = searchText - ? courtesycars.filter( - (c) => - (c.fleetnumber || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (c.vin || "").toLowerCase().includes(searchText.toLowerCase()) || - (c.year || "").toLowerCase().includes(searchText.toLowerCase()) || - (c.make || "").toLowerCase().includes(searchText.toLowerCase()) || - (c.model || "").toLowerCase().includes(searchText.toLowerCase()) || - (c.plate || "").toLowerCase().includes(searchText.toLowerCase()) || - (t(c.status) || "").toLowerCase().includes(searchText.toLowerCase()) + return t("courtesycars.labels.fuel.58"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 38: + return t("courtesycars.labels.fuel.38"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 0: + return t("courtesycars.labels.fuel.empty"); + default: + return record.fuel; + } + } + }, + { + title: t("courtesycars.labels.outwith"), + dataIndex: "outwith", + key: "outwith", + // sorter: (a, b) => alphaSort(a.model, b.model), + sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order, + render: (text, record) => + record.cccontracts.length === 1 ? ( + + {`${record.cccontracts[0].job.ro_number} - ${OwnerNameDisplayFunction(record.cccontracts[0].job)}`} + + ) : null + }, + { + title: t("contracts.fields.scheduledreturn"), + dataIndex: "scheduledreturn", + key: "scheduledreturn", + render: (text, record) => + record.cccontracts.length === 1 && ( + {record.cccontracts[0].scheduledreturn} ) - : courtesycars; + } + ]; - const items = [ - { - key: "courtesycar_inventory", - label: t("printcenter.courtesycarcontract.courtesy_car_inventory"), - onClick: () => - GenerateDocument( - { - name: TemplateList("courtesycar").courtesy_car_inventory.key, - variables: { - //id: contract.id - }, - }, - {}, - "p" - ), - }, - ]; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); + }; - const menu = {items}; + const tableData = searchText + ? courtesycars.filter( + (c) => + (c.fleetnumber || "").toLowerCase().includes(searchText.toLowerCase()) || + (c.vin || "").toLowerCase().includes(searchText.toLowerCase()) || + (c.year || "").toLowerCase().includes(searchText.toLowerCase()) || + (c.make || "").toLowerCase().includes(searchText.toLowerCase()) || + (c.model || "").toLowerCase().includes(searchText.toLowerCase()) || + (c.plate || "").toLowerCase().includes(searchText.toLowerCase()) || + (t(c.status) || "").toLowerCase().includes(searchText.toLowerCase()) + ) + : courtesycars; - return ( - - refetch()}> - - - - {t("general.labels.print")} - - - {t("courtesycars.actions.new")} - - { - setSearchText(e.target.value); - }} - value={searchText} - enterButton - /> - + const items = [ + { + key: "courtesycar_inventory", + label: t("printcenter.courtesycarcontract.courtesy_car_inventory"), + onClick: () => + GenerateDocument( + { + name: TemplateList("courtesycar").courtesy_car_inventory.key, + variables: { + //id: contract.id } - > - - - ); + }, + {}, + "p" + ) + } + ]; + + const menu = { items }; + + return ( + + refetch()}> + + + + {t("general.labels.print")} + + + {t("courtesycars.actions.new")} + + { + setSearchText(e.target.value); + }} + value={searchText} + enterButton + /> + + } + > + + + ); } diff --git a/client/src/components/csi-response-form/csi-response-form.container.jsx b/client/src/components/csi-response-form/csi-response-form.container.jsx index be8d04faf..137a25faf 100644 --- a/client/src/components/csi-response-form/csi-response-form.container.jsx +++ b/client/src/components/csi-response-form/csi-response-form.container.jsx @@ -1,57 +1,55 @@ -import {useQuery} from "@apollo/client"; -import {Card, Form, Result} from "antd"; +import { useQuery } from "@apollo/client"; +import { Card, Form, Result } from "antd"; import queryString from "query-string"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {useLocation} from "react-router-dom"; -import {QUERY_CSI_RESPONSE_BY_PK} from "../../graphql/csi.queries"; -import {DateFormatter} from "../../utils/DateFormatter"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router-dom"; +import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; +import { DateFormatter } from "../../utils/DateFormatter"; import AlertComponent from "../alert/alert.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; export default function CsiResponseFormContainer() { - const {t} = useTranslation(); - const [form] = Form.useForm(); - const searchParams = queryString.parse(useLocation().search); - const {responseid} = searchParams; - const {loading, error, data} = useQuery(QUERY_CSI_RESPONSE_BY_PK, { - variables: { - id: responseid, - }, - skip: !!!responseid, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const searchParams = queryString.parse(useLocation().search); + const { responseid } = searchParams; + const { loading, error, data } = useQuery(QUERY_CSI_RESPONSE_BY_PK, { + variables: { + id: responseid + }, + skip: !!!responseid, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - useEffect(() => { - form.resetFields(); - }, [data, form]); - - if (!!!responseid) - return ( - - - - ); - - if (loading) return ; - if (error) return ; + useEffect(() => { + form.resetFields(); + }, [data, form]); + if (!!!responseid) return ( - - - - {data.csi_by_pk.validuntil ? ( - <> - {t("csi.fields.validuntil")} - {": "} - {data.csi_by_pk.validuntil} - > - ) : null} - + + + ); + + if (loading) return ; + if (error) return ; + + return ( + + + + {data.csi_by_pk.validuntil ? ( + <> + {t("csi.fields.validuntil")} + {": "} + {data.csi_by_pk.validuntil} + > + ) : null} + + + ); } diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index e78dbbd3a..d7b49df56 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -1,131 +1,117 @@ -import {SyncOutlined} from "@ant-design/icons"; -import {Button, Card, Table} from "antd"; +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Table } from "antd"; import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Link, useLocation, useNavigate} from "react-router-dom"; -import {DateFormatter} from "../../utils/DateFormatter"; -import {pageLimit} from "../../utils/config"; -import {alphaSort, dateSort} from "../../utils/sorters"; -import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { DateFormatter } from "../../utils/DateFormatter"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, dateSort } from "../../utils/sorters"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; -export default function CsiResponseListPaginated({ - refetch, - loading, - responses, - total, - }) { - const search = queryString.parse(useLocation().search); - const {responseid} = search; - const history = useNavigate(); - const {t} = useTranslation(); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {text: ""}, - page: "", - }); +export default function CsiResponseListPaginated({ refetch, loading, responses, total }) { + const search = queryString.parse(useLocation().search); + const { responseid } = search; + const history = useNavigate(); + const { t } = useTranslation(); + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" }, + page: "" + }); + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - - sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - - {record.job.ro_number || t("general.labels.na")} - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner_name", - key: "owner_name", - sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), - OwnerNameDisplayFunction(b.job) - ), - sortOrder: state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order, - render: (text, record) => { - return record.job.ownerid ? ( - - - - ) : ( - - + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + {record.job.ro_number || t("general.labels.na")} + ) + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner_name", + key: "owner_name", + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)), + sortOrder: state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order, + render: (text, record) => { + return record.job.ownerid ? ( + + + + ) : ( + + - ); - }, - }, - { - title: t("csi.fields.completedon"), - dataIndex: "completedon", - key: "completedon", - ellipsis: true, - sorter: (a, b) => dateSort(a.completedon, b.completedon), - sortOrder: state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, - render: (text, record) => { - return record.completedon ? ( - {record.completedon} - ) : null; - }, - }, - ]; + ); + } + }, + { + title: t("csi.fields.completedon"), + dataIndex: "completedon", + key: "completedon", + ellipsis: true, + sorter: (a, b) => dateSort(a.completedon, b.completedon), + sortOrder: state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, + render: (text, record) => { + return record.completedon ? {record.completedon} : null; + } + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - setState({ - ...state, - filteredInfo: filters, - sortedInfo: sorter, - page: pagination.current, - }); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ + ...state, + filteredInfo: filters, + sortedInfo: sorter, + page: pagination.current + }); + }; - const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - search.responseid = record.id; - history({search: queryString.stringify(search)}); - } - } else { - delete search.responseid; - history({search: queryString.stringify(search)}); - } - }; + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + search.responseid = record.id; + history({ search: queryString.stringify(search) }); + } + } else { + delete search.responseid; + history({ search: queryString.stringify(search) }); + } + }; - return ( - refetch()}> - - - } - > - { - handleOnRowClick(record); - }, - selectedRowKeys: [responseid], - type: "radio", - - }} - /> - - ); + return ( + refetch()}> + + + } + > + { + handleOnRowClick(record); + }, + selectedRowKeys: [responseid], + type: "radio" + }} + /> + + ); } diff --git a/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx index 8e03f04b5..e2dee64f5 100644 --- a/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx +++ b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx @@ -1,168 +1,183 @@ -import {Card, Table, Tag} from "antd"; +import { Card, Table, Tag } from "antd"; import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; -import {useTranslation} from "react-i18next"; -import React, {useEffect, useState} from "react"; -import dayjs from '../../../utils/day'; +import { useTranslation } from "react-i18next"; +import React, { useEffect, useState } from "react"; +import dayjs from "../../../utils/day"; import DashboardRefreshRequired from "../refresh-required.component"; import axios from "axios"; -const fortyFiveDaysAgo = () => dayjs().subtract(45, 'day').toLocaleString(); +const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString(); -export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) { - console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop) - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); - const [lifecycleData, setLifecycleData] = useState(null); +export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) { + console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - async function getLifecycleData() { - if (data && data.job_lifecycle) { - setLoading(true); - const response = await axios.post("/job/lifecycle", { - jobids: data.job_lifecycle.map(x => x.id), - statuses: bodyshop.md_ro_statuses - }); - setLifecycleData(response.data.durations); - setLoading(false); - } - } + useEffect(() => { + async function getLifecycleData() { + if (data && data.job_lifecycle) { + setLoading(true); + const response = await axios.post("/job/lifecycle", { + jobids: data.job_lifecycle.map((x) => x.id), + statuses: bodyshop.md_ro_statuses + }); + setLifecycleData(response.data.durations); + setLoading(false); + } + } - getLifecycleData().catch(e => { - console.error(`Error in getLifecycleData: ${e}`); - }) - }, [data, bodyshop]); + getLifecycleData().catch((e) => { + console.error(`Error in getLifecycleData: ${e}`); + }); + }, [data, bodyshop]); - const columns = [ - { - title: t('job_lifecycle.columns.status'), - dataIndex: 'status', - bgColor: 'red', - key: 'status', - render: (text, record) => { - return {record.status} - } - }, - { - title: t('job_lifecycle.columns.human_readable'), - dataIndex: 'humanReadable', - key: 'humanReadable', - }, - { - title: t('job_lifecycle.columns.status_count'), - key: 'statusCount', - render: (text, record) => { - return lifecycleData.statusCounts[record.status]; - } - }, - { - title: t('job_lifecycle.columns.percentage'), - dataIndex: 'percentage', - key: 'percentage', - render: (text, record) => { - return record.percentage.toFixed(2) + '%'; - } - }, - ]; + const columns = [ + { + title: t("job_lifecycle.columns.status"), + dataIndex: "status", + bgColor: "red", + key: "status", + render: (text, record) => { + return {record.status}; + } + }, + { + title: t("job_lifecycle.columns.human_readable"), + dataIndex: "humanReadable", + key: "humanReadable" + }, + { + title: t("job_lifecycle.columns.status_count"), + key: "statusCount", + render: (text, record) => { + return lifecycleData.statusCounts[record.status]; + } + }, + { + title: t("job_lifecycle.columns.percentage"), + dataIndex: "percentage", + key: "percentage", + render: (text, record) => { + return record.percentage.toFixed(2) + "%"; + } + } + ]; - if (!data) return null; + if (!data) return null; - if (!data.job_lifecycle || !lifecycleData) return ; + if (!data.job_lifecycle || !lifecycleData) return ; - const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}` + const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`; - return ( - - - - - {lifecycleData.summations.map((key, index, array) => { - const isFirst = index === 0; - const isLast = index === array.length - 1; - return ( - + + + + {lifecycleData.summations.map((key, index, array) => { + const isFirst = index === 0; + const isLast = index === array.length - 1; + return ( + - - {key.percentage > 15 ? - <> - {key.roundedPercentage} - - {key.status} - - > - : null} - - ); - })} - - - - {lifecycleData.summations.map((key) => ( - - - {key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage}) - - - ))} - - - - b.value - a.value).slice(0, 3)}/> - + backgroundColor: key.color, + width: `${key.percentage}%` + }} + aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`} + title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`} + > + {key.percentage > 15 ? ( + <> + {key.roundedPercentage} + + {key.status} + + > + ) : null} - - - ); + ); + })} + + + + {lifecycleData.summations.map((key) => ( + + + {key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage}) + + + ))} + + + + b.value - a.value).slice(0, 3)} + /> + + + + + ); } export const JobLifecycleDashboardGQL = ` job_lifecycle: jobs(where: { actual_in: { - _gte: "${dayjs().subtract(45, 'day').toISOString()}" + _gte: "${dayjs().subtract(45, "day").toISOString()}" } }) { id diff --git a/client/src/components/dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx b/client/src/components/dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx index afc93350d..62370079b 100644 --- a/client/src/components/dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx +++ b/client/src/components/dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx @@ -1,159 +1,124 @@ -import {Card} from "antd"; +import { Card } from "antd"; import _ from "lodash"; import dayjs from "../../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis,} from "recharts"; +import { useTranslation } from "react-i18next"; +import { Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardMonthlyEmployeeEfficiency({ - data, - ...cardProps - }) { - const {t} = useTranslation(); - if (!data) return null; - if (!data.monthly_employee_efficiency) - return ; +export default function DashboardMonthlyEmployeeEfficiency({ data, ...cardProps }) { + const { t } = useTranslation(); + if (!data) return null; + if (!data.monthly_employee_efficiency) return ; - const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) => - dayjs(item.date).format("YYYY-MM-DD") - ); + const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) => dayjs(item.date).format("YYYY-MM-DD")); - const listOfDays = Utils.ListOfDaysInCurrentMonth(); + const listOfDays = Utils.ListOfDaysInCurrentMonth(); - const chartData = listOfDays.reduce((acc, val) => { - //Sum up the current day. - let dailyHrs; - if (!!ticketsByDate[val]) { - dailyHrs = ticketsByDate[val].reduce( - (dayAcc, dayVal) => { - return { - actual: dayAcc.actual + dayVal.actualhrs, - productive: dayAcc.productive + dayVal.productivehrs, - }; - }, - {actual: 0, productive: 0} - ); - } else { - dailyHrs = {actual: 0, productive: 0}; - } + const chartData = listOfDays.reduce((acc, val) => { + //Sum up the current day. + let dailyHrs; + if (!!ticketsByDate[val]) { + dailyHrs = ticketsByDate[val].reduce( + (dayAcc, dayVal) => { + return { + actual: dayAcc.actual + dayVal.actualhrs, + productive: dayAcc.productive + dayVal.productivehrs + }; + }, + { actual: 0, productive: 0 } + ); + } else { + dailyHrs = { actual: 0, productive: 0 }; + } - const dailyEfficiency = - ((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100; + const dailyEfficiency = ((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100; - const theValue = { - date: dayjs(val).format("DD"), - // ...dailyHrs, - actual: dailyHrs.actual.toFixed(1), - productive: dailyHrs.productive.toFixed(1), - dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1), - accActual: - acc.length > 0 - ? acc[acc.length - 1].accActual + dailyHrs.actual - : dailyHrs.actual, + const theValue = { + date: dayjs(val).format("DD"), + // ...dailyHrs, + actual: dailyHrs.actual.toFixed(1), + productive: dailyHrs.productive.toFixed(1), + dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1), + accActual: acc.length > 0 ? acc[acc.length - 1].accActual + dailyHrs.actual : dailyHrs.actual, - accProductive: - acc.length > 0 - ? acc[acc.length - 1].accProductive + dailyHrs.productive - : dailyHrs.productive, - accEfficiency: 0, - }; + accProductive: acc.length > 0 ? acc[acc.length - 1].accProductive + dailyHrs.productive : dailyHrs.productive, + accEfficiency: 0 + }; - theValue.accEfficiency = - ((theValue.accProductive - theValue.accActual) / - (theValue.accActual || 0) + - 1) * - 100; + theValue.accEfficiency = ((theValue.accProductive - theValue.accActual) / (theValue.accActual || 0) + 1) * 100; - if (isNaN(theValue.accEfficiency)) { - theValue.accEfficiency = 0; - } else { - theValue.accEfficiency = theValue.accEfficiency.toFixed(1); - } + if (isNaN(theValue.accEfficiency)) { + theValue.accEfficiency = 0; + } else { + theValue.accEfficiency = theValue.accEfficiency.toFixed(1); + } - return [...acc, theValue]; - }, []); + return [...acc, theValue]; + }, []); - return ( - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + ); } export const DashboardMonthlyEmployeeEfficiencyGql = ` monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${dayjs() .startOf("month") - .format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs() - .endOf("month") - .format("YYYY-MM-DD")}"}} ]}) { + .format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs().endOf("month").format("YYYY-MM-DD")}"}} ]}) { actualhrs productivehrs employeeid diff --git a/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx b/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx index deeabbcc3..3924ccb37 100644 --- a/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx +++ b/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx @@ -1,164 +1,138 @@ -import {Card, Input, Space, Table, Typography} from "antd"; +import { Card, Input, Space, Table, Typography } from "antd"; import axios from "axios"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {alphaSort} from "../../../utils/sorters"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { alphaSort } from "../../../utils/sorters"; import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; import Dinero from "dinero.js"; import DashboardRefreshRequired from "../refresh-required.component"; -import {pageLimit} from "../../../utils/config"; +import { pageLimit } from "../../../utils/config"; -export default function DashboardMonthlyJobCosting({data, ...cardProps}) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); - const [costingData, setcostingData] = useState(null); - const [searchText, setSearchText] = useState(""); - const [state, setState] = useState({ - sortedInfo: {}, - }); +export default function DashboardMonthlyJobCosting({ data, ...cardProps }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [costingData, setcostingData] = useState(null); + const [searchText, setSearchText] = useState(""); + const [state, setState] = useState({ + sortedInfo: {} + }); - useEffect(() => { - async function getCostingData() { - if (data && data.monthly_sales) { - setLoading(true); - const response = await axios.post("/job/costingmulti", { - jobids: data.monthly_sales.map((x) => x.id), - }); - setcostingData(response.data); - setLoading(false); - } - } + useEffect(() => { + async function getCostingData() { + if (data && data.monthly_sales) { + setLoading(true); + const response = await axios.post("/job/costingmulti", { + jobids: data.monthly_sales.map((x) => x.id) + }); + setcostingData(response.data); + setLoading(false); + } + } - getCostingData(); - }, [data]); + getCostingData(); + }, [data]); - if (!data) return null; - if (!data.monthly_sales) return ; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; - const columns = [ - { - title: t("bodyshop.fields.responsibilitycenter"), - dataIndex: "cost_center", - key: "cost_center", - sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), - sortOrder: - state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, - }, - { - title: t("jobs.labels.sales"), - dataIndex: "sales", - key: "sales", - sorter: (a, b) => - parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "sales" && state.sortedInfo.order, - }, + if (!data) return null; + if (!data.monthly_sales) return ; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + const columns = [ + { + title: t("bodyshop.fields.responsibilitycenter"), + dataIndex: "cost_center", + key: "cost_center", + sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), + sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order + }, + { + title: t("jobs.labels.sales"), + dataIndex: "sales", + key: "sales", + sorter: (a, b) => parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)), + sortOrder: state.sortedInfo.columnKey === "sales" && state.sortedInfo.order + }, - { - title: t("jobs.labels.costs"), - dataIndex: "costs", - key: "costs", - sorter: (a, b) => - parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "costs" && state.sortedInfo.order, - }, + { + title: t("jobs.labels.costs"), + dataIndex: "costs", + key: "costs", + sorter: (a, b) => parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)), + sortOrder: state.sortedInfo.columnKey === "costs" && state.sortedInfo.order + }, - { - title: t("jobs.labels.gpdollars"), - dataIndex: "gpdollars", - key: "gpdollars", - sorter: (a, b) => - parseFloat(a.gpdollars.substring(1)) - - parseFloat(b.gpdollars.substring(1)), + { + title: t("jobs.labels.gpdollars"), + dataIndex: "gpdollars", + key: "gpdollars", + sorter: (a, b) => parseFloat(a.gpdollars.substring(1)) - parseFloat(b.gpdollars.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order, - }, - { - title: t("jobs.labels.gppercent"), - dataIndex: "gppercent", - key: "gppercent", - sorter: (a, b) => - parseFloat(a.gppercent.slice(0, -1) || 0) - - parseFloat(b.gppercent.slice(0, -1) || 0), - sortOrder: - state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order, - }, - ]; - const filteredData = - searchText === "" - ? (costingData && costingData.allCostCenterData) || [] - : costingData.allCostCenterData.filter((d) => - (d.cost_center || "") - .toString() - .toLowerCase() - .includes(searchText.toLowerCase()) - ); + sortOrder: state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order + }, + { + title: t("jobs.labels.gppercent"), + dataIndex: "gppercent", + key: "gppercent", + sorter: (a, b) => parseFloat(a.gppercent.slice(0, -1) || 0) - parseFloat(b.gppercent.slice(0, -1) || 0), + sortOrder: state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order + } + ]; + const filteredData = + searchText === "" + ? (costingData && costingData.allCostCenterData) || [] + : costingData.allCostCenterData.filter((d) => + (d.cost_center || "").toString().toLowerCase().includes(searchText.toLowerCase()) + ); - return ( - - { - e.preventDefault(); - setSearchText(e.target.value); - }} - /> - - } - {...cardProps} - > - - - ( - - - - {t("general.labels.totals")} - - - - {Dinero( - costingData && - costingData.allSummaryData && - costingData.allSummaryData.totalSales - ).toFormat()} - - - {Dinero( - costingData && - costingData.allSummaryData && - costingData.allSummaryData.totalCost - ).toFormat()} - - - {Dinero( - costingData && - costingData.allSummaryData && - costingData.allSummaryData.gpdollars - ).toFormat()} - - - - )} - /> - - - - ); + return ( + + { + e.preventDefault(); + setSearchText(e.target.value); + }} + /> + + } + {...cardProps} + > + + + ( + + + {t("general.labels.totals")} + + + {Dinero( + costingData && costingData.allSummaryData && costingData.allSummaryData.totalSales + ).toFormat()} + + + {Dinero(costingData && costingData.allSummaryData && costingData.allSummaryData.totalCost).toFormat()} + + + {Dinero(costingData && costingData.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()} + + + + )} + /> + + + + ); } diff --git a/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx b/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx index cd4f13e71..ca719b891 100644 --- a/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx +++ b/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx @@ -1,67 +1,64 @@ -import {Card} from "antd"; +import { Card } from "antd"; import Dinero from "dinero.js"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Cell, Pie, PieChart, ResponsiveContainer, Sector} from "recharts"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardMonthlyLaborSales({data, ...cardProps}) { - const {t} = useTranslation(); - const [activeIndex, setActiveIndex] = useState(0); - if (!data) return null; - if (!data.monthly_sales) return ; +export default function DashboardMonthlyLaborSales({ data, ...cardProps }) { + const { t } = useTranslation(); + const [activeIndex, setActiveIndex] = useState(0); + if (!data) return null; + if (!data.monthly_sales) return ; - const laborData = {}; + const laborData = {}; - data.monthly_sales.forEach((job) => { - job.joblines.forEach((jobline) => { - if (!jobline.mod_lbr_ty) return; - if (!laborData[jobline.mod_lbr_ty]) - laborData[jobline.mod_lbr_ty] = Dinero(); - laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add( - Dinero({ - amount: Math.round( - (job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100 - ), - }).multiply(jobline.mod_lb_hrs || 0) - ); - }); + data.monthly_sales.forEach((job) => { + job.joblines.forEach((jobline) => { + if (!jobline.mod_lbr_ty) return; + if (!laborData[jobline.mod_lbr_ty]) laborData[jobline.mod_lbr_ty] = Dinero(); + laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add( + Dinero({ + amount: Math.round((job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100) + }).multiply(jobline.mod_lb_hrs || 0) + ); }); + }); - const chartData = Object.keys(laborData).map((key) => { - return { - name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`), - value: laborData[key].getAmount() / 100, - color: pieColor(key.toUpperCase()), - }; - }); + const chartData = Object.keys(laborData).map((key) => { + return { + name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`), + value: laborData[key].getAmount() / 100, + color: pieColor(key.toUpperCase()) + }; + }); - return ( - - - - - setActiveIndex(index)} - > - {chartData.map((entry, index) => ( - - ))} - - - - - - ); + return ( + + + + + setActiveIndex(index)} + > + {chartData.map((entry, index) => ( + + ))} + + + + + + ); } export const DashboardMonthlyRevenueGraphGql = ` @@ -69,75 +66,75 @@ export const DashboardMonthlyRevenueGraphGql = ` `; const pieColor = (type) => { - if (type === "LAA") return "lightgreen"; - else if (type === "LAB") return "dodgerblue"; - else if (type === "LAD") return "aliceblue"; - else if (type === "LAE") return "seafoam"; - else if (type === "LAG") return "chartreuse"; - else if (type === "LAF") return "magenta"; - else if (type === "LAM") return "gold"; - else if (type === "LAR") return "crimson"; - else if (type === "LAU") return "slategray"; - else if (type === "LA1") return "slategray"; - else if (type === "LA2") return "slategray"; - else if (type === "LA3") return "slategray"; - else if (type === "LA4") return "slategray"; - return "slategray"; + if (type === "LAA") return "lightgreen"; + else if (type === "LAB") return "dodgerblue"; + else if (type === "LAD") return "aliceblue"; + else if (type === "LAE") return "seafoam"; + else if (type === "LAG") return "chartreuse"; + else if (type === "LAF") return "magenta"; + else if (type === "LAM") return "gold"; + else if (type === "LAR") return "crimson"; + else if (type === "LAU") return "slategray"; + else if (type === "LA1") return "slategray"; + else if (type === "LA2") return "slategray"; + else if (type === "LA3") return "slategray"; + else if (type === "LA4") return "slategray"; + return "slategray"; }; const renderActiveShape = (props) => { - //const RADIAN = Math.PI / 180; - const { - cx, - cy, - //midAngle, - innerRadius, - outerRadius, - startAngle, - endAngle, - fill, - payload, - // percent, - value, - } = props; - // const sin = Math.sin(-RADIAN * midAngle); - // const cos = Math.cos(-RADIAN * midAngle); - // // const sx = cx + (outerRadius + 10) * cos; - // const sy = cy + (outerRadius + 10) * sin; - // const mx = cx + (outerRadius + 30) * cos; - // const my = cy + (outerRadius + 30) * sin; - // //const ex = mx + (cos >= 0 ? 1 : -1) * 22; - // const ey = my; - //const textAnchor = cos >= 0 ? "start" : "end"; + //const RADIAN = Math.PI / 180; + const { + cx, + cy, + //midAngle, + innerRadius, + outerRadius, + startAngle, + endAngle, + fill, + payload, + // percent, + value + } = props; + // const sin = Math.sin(-RADIAN * midAngle); + // const cos = Math.cos(-RADIAN * midAngle); + // // const sx = cx + (outerRadius + 10) * cos; + // const sy = cy + (outerRadius + 10) * sin; + // const mx = cx + (outerRadius + 30) * cos; + // const my = cy + (outerRadius + 30) * sin; + // //const ex = mx + (cos >= 0 ? 1 : -1) * 22; + // const ey = my; + //const textAnchor = cos >= 0 ? "start" : "end"; - return ( - - - {payload.name} - - - {Dinero({amount: Math.round(value * 100)}).toFormat()} - - - - - ); + return ( + + + {payload.name} + + + {Dinero({ amount: Math.round(value * 100) }).toFormat()} + + + + + ); }; // ; +export default function DashboardMonthlyPartsSales({ data, ...cardProps }) { + const { t } = useTranslation(); + const [activeIndex, setActiveIndex] = useState(0); + if (!data) return null; + if (!data.monthly_sales) return ; - const partData = {}; + const partData = {}; - data.monthly_sales.forEach((job) => { - job.joblines.forEach((jobline) => { - if (!jobline.part_type) return; - if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero(); - partData[jobline.part_type] = partData[jobline.part_type].add( - Dinero({amount: Math.round((jobline.act_price || 0) * 100)}).multiply( - jobline.part_qty || 0 - ) - ); - }); + data.monthly_sales.forEach((job) => { + job.joblines.forEach((jobline) => { + if (!jobline.part_type) return; + if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero(); + partData[jobline.part_type] = partData[jobline.part_type].add( + Dinero({ amount: Math.round((jobline.act_price || 0) * 100) }).multiply(jobline.part_qty || 0) + ); }); + }); - const chartData = Object.keys(partData).map((key) => { - return { - name: t(`joblines.fields.part_types.${key.toUpperCase()}`), - value: partData[key].getAmount() / 100, - color: pieColor(key.toUpperCase()), - }; - }); + const chartData = Object.keys(partData).map((key) => { + return { + name: t(`joblines.fields.part_types.${key.toUpperCase()}`), + value: partData[key].getAmount() / 100, + color: pieColor(key.toUpperCase()) + }; + }); - return ( - - - - - setActiveIndex(index)} - > - {chartData.map((entry, index) => ( - - ))} - - - - - - ); + return ( + + + + + setActiveIndex(index)} + > + {chartData.map((entry, index) => ( + + ))} + + + + + + ); } export const DashboardMonthlyRevenueGraphGql = ` `; const pieColor = (type) => { - if (type === "PAA") return "darkgreen"; - else if (type === "PAC") return "green"; - else if (type === "PAE") return "gold"; - else if (type === "PAG") return "seafoam"; - else if (type === "PAL") return "chartreuse"; - else if (type === "PAM") return "magenta"; - else if (type === "PAN") return "crimson"; - else if (type === "PAO") return "gold"; - else if (type === "PAP") return "crimson"; - else if (type === "PAR") return "indigo"; - else if (type === "PAS") return "dodgerblue"; - else if (type === "PASL") return "dodgerblue"; - return "slategray"; + if (type === "PAA") return "darkgreen"; + else if (type === "PAC") return "green"; + else if (type === "PAE") return "gold"; + else if (type === "PAG") return "seafoam"; + else if (type === "PAL") return "chartreuse"; + else if (type === "PAM") return "magenta"; + else if (type === "PAN") return "crimson"; + else if (type === "PAO") return "gold"; + else if (type === "PAP") return "crimson"; + else if (type === "PAR") return "indigo"; + else if (type === "PAS") return "dodgerblue"; + else if (type === "PASL") return "dodgerblue"; + return "slategray"; }; const renderActiveShape = (props) => { - // const RADIAN = Math.PI / 180; - const { - cx, - cy, - // midAngle, - innerRadius, - outerRadius, - startAngle, - endAngle, - fill, - payload, - // percent, - value, - } = props; - // const sin = Math.sin(-RADIAN * midAngle); - // const cos = Math.cos(-RADIAN * midAngle); - // const sx = cx + (outerRadius + 10) * cos; - //const sy = cy + (outerRadius + 10) * sin; - // const mx = cx + (outerRadius + 30) * cos; - //const my = cy + (outerRadius + 30) * sin; - // const ex = mx + (cos >= 0 ? 1 : -1) * 22; - // const ey = my; - // const textAnchor = cos >= 0 ? "start" : "end"; + // const RADIAN = Math.PI / 180; + const { + cx, + cy, + // midAngle, + innerRadius, + outerRadius, + startAngle, + endAngle, + fill, + payload, + // percent, + value + } = props; + // const sin = Math.sin(-RADIAN * midAngle); + // const cos = Math.cos(-RADIAN * midAngle); + // const sx = cx + (outerRadius + 10) * cos; + //const sy = cy + (outerRadius + 10) * sin; + // const mx = cx + (outerRadius + 30) * cos; + //const my = cy + (outerRadius + 30) * sin; + // const ex = mx + (cos >= 0 ? 1 : -1) * 22; + // const ey = my; + // const textAnchor = cos >= 0 ? "start" : "end"; - return ( - - - {payload.name} - - - {Dinero({amount: Math.round(value * 100)}).toFormat()} - - - - - ); + return ( + + + {payload.name} + + + {Dinero({ amount: Math.round(value * 100) }).toFormat()} + + + + + ); }; diff --git a/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx index d5e9c9627..4f9f5ac4c 100644 --- a/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx +++ b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx @@ -1,83 +1,66 @@ -import {Card} from "antd"; +import { Card } from "antd"; import dayjs from "../../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import _ from "lodash"; -import {Area, Bar, CartesianGrid, ComposedChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis,} from "recharts"; +import { Area, Bar, CartesianGrid, ComposedChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import Dinero from "dinero.js"; import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardMonthlyRevenueGraph({data, ...cardProps}) { - const {t} = useTranslation(); - if (!data) return null; - if (!data.monthly_sales) return ; +export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) { + const { t } = useTranslation(); + if (!data) return null; + if (!data.monthly_sales) return ; - const jobsByDate = _.groupBy(data.monthly_sales, (item) => - dayjs(item.date_invoiced).format("YYYY-MM-DD") - ); + const jobsByDate = _.groupBy(data.monthly_sales, (item) => dayjs(item.date_invoiced).format("YYYY-MM-DD")); - const listOfDays = Utils.ListOfDaysInCurrentMonth(); + const listOfDays = Utils.ListOfDaysInCurrentMonth(); - const chartData = listOfDays.reduce((acc, val) => { - //Sum up the current day. - let dailySales; - if (!!jobsByDate[val]) { - dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => { - return dayAcc.add( - Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0) - ); - }, Dinero()); - } else { - dailySales = Dinero(); - } + const chartData = listOfDays.reduce((acc, val) => { + //Sum up the current day. + let dailySales; + if (!!jobsByDate[val]) { + dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => { + return dayAcc.add(Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)); + }, Dinero()); + } else { + dailySales = Dinero(); + } - const theValue = { - date: dayjs(val).format("DD"), - dailySales: dailySales.getAmount() / 100, - accSales: - acc.length > 0 - ? acc[acc.length - 1].accSales + dailySales.getAmount() / 100 - : dailySales.getAmount() / 100, - }; + const theValue = { + date: dayjs(val).format("DD"), + dailySales: dailySales.getAmount() / 100, + accSales: + acc.length > 0 ? acc[acc.length - 1].accSales + dailySales.getAmount() / 100 : dailySales.getAmount() / 100 + }; - return [...acc, theValue]; - }, []); + return [...acc, theValue]; + }, []); - return ( - - - - - - - - value && value.toFixed(2)} - /> - - - - - - - - ); + return ( + + + + + + + + value && value.toFixed(2)} /> + + + + + + + + ); } export const DashboardMonthlyRevenueGraphGql = ` diff --git a/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx b/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx index d315fda6e..2845bc858 100644 --- a/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx +++ b/client/src/components/dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx @@ -1,34 +1,26 @@ -import {Card, Statistic} from "antd"; +import { Card, Statistic } from "antd"; import Dinero from "dinero.js"; import dayjs from "../../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardProjectedMonthlySales({data, ...cardProps}) { - const {t} = useTranslation(); - if (!data) return null; - if (!data.projected_monthly_sales) - return ; +export default function DashboardProjectedMonthlySales({ data, ...cardProps }) { + const { t } = useTranslation(); + if (!data) return null; + if (!data.projected_monthly_sales) return ; - const dollars = - data.projected_monthly_sales && - data.projected_monthly_sales.reduce( - (acc, val) => - acc.add( - Dinero( - val.job_totals && - val.job_totals.totals && - val.job_totals.totals.subtotal - ) - ), - Dinero() - ); - return ( - - - + const dollars = + data.projected_monthly_sales && + data.projected_monthly_sales.reduce( + (acc, val) => acc.add(Dinero(val.job_totals && val.job_totals.totals && val.job_totals.totals.subtotal)), + Dinero() ); + return ( + + + + ); } export const DashboardProjectedMonthlySalesGql = ` @@ -38,12 +30,9 @@ export const DashboardProjectedMonthlySalesGql = ` {_and: [ {date_invoiced:{_is_null: false }}, {date_invoiced: {_gte: "${dayjs() - .startOf("month") - .startOf("day") - .toISOString()}"}}, {date_invoiced: {_lte: "${dayjs() - .endOf("month") - .endOf("day") - .toISOString()}"}}]}, + .startOf("month") + .startOf("day") + .toISOString()}"}}, {date_invoiced: {_lte: "${dayjs().endOf("month").endOf("day").toISOString()}"}}]}, { _and:[ @@ -51,10 +40,7 @@ _and:[ {actual_completion: {_gte: "${dayjs() .startOf("month") .startOf("day") - .toISOString()}"}}, {actual_completion: {_lte: "${dayjs() - .endOf("month") - .endOf("day") - .toISOString()}"}} + .toISOString()}"}}, {actual_completion: {_lte: "${dayjs().endOf("month").endOf("day").toISOString()}"}} ] }, @@ -63,12 +49,9 @@ _and:[ {date_invoiced: {_is_null: true}}, {actual_completion: {_is_null: true}} {scheduled_completion: {_gte: "${dayjs() - .startOf("month") - .startOf("day") - .toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs() - .endOf("month") - .endOf("day") - .toISOString()}"}} + .startOf("month") + .startOf("day") + .toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs().endOf("month").endOf("day").toISOString()}"}} ]} diff --git a/client/src/components/dashboard-components/refresh-required.component.jsx b/client/src/components/dashboard-components/refresh-required.component.jsx index f4614e025..bae6f0e8e 100644 --- a/client/src/components/dashboard-components/refresh-required.component.jsx +++ b/client/src/components/dashboard-components/refresh-required.component.jsx @@ -1,25 +1,25 @@ -import {SyncOutlined} from "@ant-design/icons"; -import {Card} from "antd"; +import { SyncOutlined } from "@ant-design/icons"; +import { Card } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; export default function DashboardRefreshRequired(props) { - const {t} = useTranslation(); + const { t } = useTranslation(); - return ( - - - - {t("dashboard.errors.refreshrequired")} - - - ); + return ( + + + + {t("dashboard.errors.refreshrequired")} + + + ); } diff --git a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx index 5d24bb606..d3caeaca2 100644 --- a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx @@ -1,8 +1,4 @@ -import { - BranchesOutlined, - ExclamationCircleFilled, - PauseCircleOutlined, -} from "@ant-design/icons"; +import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons"; import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import dayjs from "../../../utils/day"; import React, { useState } from "react"; @@ -13,26 +9,20 @@ import { onlyUnique } from "../../../utils/arrayHelper"; import { alphaSort, dateSort } from "../../../utils/sorters"; import useLocalStorage from "../../../utils/useLocalStorage"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay, { - OwnerNameDisplayFunction, -} from "../../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component"; import DashboardRefreshRequired from "../refresh-required.component"; export default function DashboardScheduledInToday({ data, ...cardProps }) { const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, - filteredInfo: {}, + filteredInfo: {} }); - const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage( - "isTvModeScheduledIn", - false - ); + const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage("isTvModeScheduledIn", false); if (!data) return null; - if (!data.scheduled_in_today) - return ; + if (!data.scheduled_in_today) return ; const appt = []; // Flatten Data data.scheduled_in_today.forEach((item) => { @@ -68,13 +58,13 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { vehicleid: item.job.vehicleid, note: item.note, start: item.start, - title: item.title, + title: item.title }; appt.push(i); } }); appt.sort(function (a, b) { - return dayjs(a.start) - dayjs(b.start); + return dayjs(a.start) - dayjs(b.start); }); const tvFontSize = 16; @@ -87,35 +77,28 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { key: "start", ellipsis: true, sorter: (a, b) => dateSort(a.start, b.start), - sortOrder: - state.sortedInfo.columnKey === "start" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "start" && state.sortedInfo.order, render: (text, record) => ( {record.start} - ), + ) }, { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( - e.stopPropagation()} - > + e.stopPropagation()}> {record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? ( ) : null} - {record.suspended && ( - - )} + {record.suspended && } {record.iouparent && ( @@ -124,23 +107,18 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { - ), + ) }, { title: t("jobs.fields.owner"), dataIndex: "owner", key: "owner", ellipsis: true, - sorter: (a, b) => - alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( - e.stopPropagation()} - > + e.stopPropagation()}> @@ -150,7 +128,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ); - }, + } }, { title: t("jobs.fields.vehicle"), @@ -159,23 +137,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ellipsis: true, sorter: (a, b) => alphaSort( - `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ - a.v_model_desc || "" - }`, + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` ), - sortOrder: - state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { return record.vehicleid ? ( - e.stopPropagation()} - > + e.stopPropagation()}> - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ) : ( @@ -183,7 +153,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { record.v_model_yr || "" } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ); - }, + } }, { title: t("appointments.fields.alt_transport"), @@ -191,9 +161,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { key: "alt_transport", ellipsis: true, sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), - sortOrder: - state.sortedInfo.columnKey === "alt_transport" && - state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order, filters: (appt && appt @@ -202,47 +170,38 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { .map((s) => { return { text: s || "No Alt. Transport", - value: [s], + value: [s] }; }) .sort((a, b) => alphaSort(a.text, b.text))) || [], onFilter: (value, record) => value.includes(record.alt_transport), render: (text, record) => ( - - {record.alt_transport} - - ), + {record.alt_transport} + ) }, { title: t("jobs.fields.lab"), dataIndex: "joblines_body", key: "joblines_body", sorter: (a, b) => a.joblines_body - b.joblines_body, - sortOrder: - state.sortedInfo.columnKey === "joblines_body" && - state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_body.toFixed(1)} - - ), + {record.joblines_body.toFixed(1)} + ) }, { title: t("jobs.fields.lar"), dataIndex: "joblines_ref", key: "joblines_ref", sorter: (a, b) => a.joblines_ref - b.joblines_ref, - sortOrder: - state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, align: "right", render: (text, record) => ( - - {record.joblines_ref.toFixed(1)} - - ), - }, + {record.joblines_ref.toFixed(1)} + ) + } ]; const columns = [ @@ -252,30 +211,23 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { key: "start", ellipsis: true, sorter: (a, b) => dateSort(a.start, b.start), - sortOrder: - state.sortedInfo.columnKey === "start" && state.sortedInfo.order, - render: (text, record) => {record.start}, + sortOrder: state.sortedInfo.columnKey === "start" && state.sortedInfo.order, + render: (text, record) => {record.start} }, { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( - e.stopPropagation()} - > + e.stopPropagation()}> {record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? ( ) : null} - {record.suspended && ( - - )} + {record.suspended && } {record.iouparent && ( @@ -283,23 +235,18 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { )} - ), + ) }, { title: t("jobs.fields.owner"), dataIndex: "owner", key: "owner", ellipsis: true, - sorter: (a, b) => - alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( - e.stopPropagation()} - > + e.stopPropagation()}> ) : ( @@ -307,7 +254,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ); - }, + } }, { title: t("dashboard.labels.phone"), @@ -320,7 +267,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { - ), + ) }, { title: t("jobs.fields.ownr_ea"), @@ -328,9 +275,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { key: "ownr_ea", ellipsis: true, responsive: ["md"], - render: (text, record) => ( - {record.ownr_ea} - ), + render: (text, record) => {record.ownr_ea} }, { title: t("jobs.fields.vehicle"), @@ -339,29 +284,19 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ellipsis: true, sorter: (a, b) => alphaSort( - `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ - a.v_model_desc || "" - }`, + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` ), - sortOrder: - state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { return record.vehicleid ? ( - e.stopPropagation()} - > - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + e.stopPropagation()}> + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} ); - }, + } }, { title: t("jobs.fields.ins_co_nm"), @@ -370,8 +305,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ellipsis: true, responsive: ["md"], sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), - sortOrder: - state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, filters: (appt && appt @@ -380,12 +314,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { .map((s) => { return { text: s || "No Ins. Co.*", - value: [s], + value: [s] }; }) .sort((a, b) => alphaSort(a.text, b.text))) || [], - onFilter: (value, record) => value.includes(record.ins_co_nm), + onFilter: (value, record) => value.includes(record.ins_co_nm) }, { title: t("appointments.fields.alt_transport"), @@ -393,9 +327,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { key: "alt_transport", ellipsis: true, sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), - sortOrder: - state.sortedInfo.columnKey === "alt_transport" && - state.sortedInfo.order, + sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order, filters: (appt && appt @@ -404,13 +336,13 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { .map((s) => { return { text: s || "No Alt. Transport", - value: [s], + value: [s] }; }) .sort((a, b) => alphaSort(a.text, b.text))) || [], - onFilter: (value, record) => value.includes(record.alt_transport), - }, + onFilter: (value, record) => value.includes(record.alt_transport) + } ]; const handleTableChange = (pagination, filters, sorter) => { @@ -419,15 +351,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { return ( {t("general.labels.tvmode")} - setIsTvModeScheduledIn(!isTvModeScheduledIn)} - defaultChecked={isTvModeScheduledIn} - /> + setIsTvModeScheduledIn(!isTvModeScheduledIn)} defaultChecked={isTvModeScheduledIn} /> } {...cardProps} @@ -449,11 +378,9 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { } export const DashboardScheduledInTodayGql = ` - scheduled_in_today: appointments(where: {start: {_gte: "${dayjs() - .startOf("day") - .toISOString()}", _lte: "${dayjs() - .endOf("day") - .toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) { + scheduled_in_today: appointments(where: {start: {_gte: "${dayjs().startOf("day").toISOString()}", _lte: "${dayjs() + .endOf("day") + .toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) { canceled id job { diff --git a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx index 1639c8fe9..55a783e8e 100644 --- a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx @@ -1,452 +1,382 @@ -import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined,} from "@ant-design/icons"; -import {Card, Space, Switch, Table, Tooltip, Typography} from "antd"; +import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons"; +import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import dayjs from "../../../utils/day"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Link} from "react-router-dom"; -import {TimeFormatter} from "../../../utils/DateFormatter"; -import {onlyUnique} from "../../../utils/arrayHelper"; -import {alphaSort, dateSort} from "../../../utils/sorters"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { TimeFormatter } from "../../../utils/DateFormatter"; +import { onlyUnique } from "../../../utils/arrayHelper"; +import { alphaSort, dateSort } from "../../../utils/sorters"; import useLocalStorage from "../../../utils/useLocalStorage"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardScheduledOutToday({data, ...cardProps}) { - const {t} = useTranslation(); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: {},}); - const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage( - "isTvModeScheduledOut", - false - ); - if (!data) return null; - if (!data.scheduled_out_today) - return ; +export default function DashboardScheduledOutToday({ data, ...cardProps }) { + const { t } = useTranslation(); + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: {} + }); + const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage("isTvModeScheduledOut", false); + if (!data) return null; + if (!data.scheduled_out_today) return ; - const scheduledOutToday = data.scheduled_out_today.map((item) => { - const joblines_body = item.joblines - ? item.joblines - .filter((l) => l.mod_lbr_ty !== "LAR") - .reduce((acc, val) => acc + val.mod_lb_hrs, 0) - : 0; - const joblines_ref = item.joblines - ? item.joblines - .filter((l) => l.mod_lbr_ty === "LAR") - .reduce((acc, val) => acc + val.mod_lb_hrs, 0) - : 0; - return { - ...item, - joblines_body, - joblines_ref, - }; - }); + const scheduledOutToday = data.scheduled_out_today.map((item) => { + const joblines_body = item.joblines + ? item.joblines.filter((l) => l.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; + const joblines_ref = item.joblines + ? item.joblines.filter((l) => l.mod_lbr_ty === "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; + return { + ...item, + joblines_body, + joblines_ref + }; + }); - console.log('Scheduled Out Today') - console.dir(scheduledOutToday); + console.log("Scheduled Out Today"); + console.dir(scheduledOutToday); - const tvFontSize = 18; - const tvFontWeight = "bold"; + const tvFontSize = 18; + const tvFontWeight = "bold"; - const tvColumns = [ - { - title: t("jobs.fields.scheduled_completion"), - dataIndex: "scheduled_completion", - key: "scheduled_completion", - ellipsis: true, - sorter: (a, b) => - dateSort(a.scheduled_completion, b.scheduled_completion), - sortOrder: - state.sortedInfo.columnKey === "scheduled_completion" && - state.sortedInfo.order, - render: (text, record) => ( - + const tvColumns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, + render: (text, record) => ( + {record.scheduled_completion} - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( - e.stopPropagation()} - > - - + ) + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()}> + + {record.ro_number || t("general.labels.na")} - {record.production_vars && record.production_vars.alert ? ( - - ) : null} - {record.suspended && ( - - )} - {record.iouparent && ( - - - - )} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && } + {record.iouparent && ( + + + + )} - - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => - alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - console.log('Render record out today'); - console.dir(record); - return record.ownerid ? ( - e.stopPropagation()} - > - - + + + ) + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + console.log("Render record out today"); + console.dir(record); + return record.ownerid ? ( + e.stopPropagation()}> + + - - ) : ( - - + + ) : ( + + - ); - }, - }, - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - sorter: (a, b) => - alphaSort( - `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ - a.v_model_desc || "" - }`, - `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` - ), - sortOrder: - state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, - render: (text, record) => { - return record.vehicleid ? ( - e.stopPropagation()} - > - - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} + ); + } + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()}> + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} - - ) : ( - {`${ - record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("appointments.fields.alt_transport"), - dataIndex: "alt_transport", - key: "alt_transport", - ellipsis: true, - sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), - sortOrder: - state.sortedInfo.columnKey === "alt_transport" && - state.sortedInfo.order, - filters: - (scheduledOutToday && - scheduledOutToday - .map((j) => j.alt_transport) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Alt. Transport*", - value: [s], - }; - }) - .sort((a, b) => alphaSort(a.text, b.text))) || - [],onFilter: (value, record) => value.includes(record.alt_transport), - render: (text, record) => ( - - {record.alt_transport} - - ), - }, - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - ellipsis: true, - sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - filters: - (scheduledOutToday && - scheduledOutToday - .map((j) => j.status) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Status*", - value: [s], - }; - }) - .sort((a, b) => alphaSort(a.text, b.text))) || - [], - onFilter: (value, record) => value.includes(record.status),render: (text, record) => ( - - {record.status} - - ), - }, - { - title: t("jobs.fields.lab"), - dataIndex: "joblines_body", - key: "joblines_body", - sorter: (a, b) => a.joblines_body - b.joblines_body, - sortOrder: - state.sortedInfo.columnKey === "joblines_body" && - state.sortedInfo.order, - align: "right", - render: (text, record) => ( - - {record.joblines_body.toFixed(1)} - - ), - }, - { - title: t("jobs.fields.lar"), - dataIndex: "joblines_ref", - key: "joblines_ref", - sorter: (a, b) => a.joblines_ref - b.joblines_ref, - sortOrder: - state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, - align: "right", - render: (text, record) => ( - - {record.joblines_ref.toFixed(1)} - - ), - }, - ]; + + ) : ( + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ); + } + }, + { + title: t("appointments.fields.alt_transport"), + dataIndex: "alt_transport", + key: "alt_transport", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order, + filters: + (scheduledOutToday && + scheduledOutToday + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s] + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport), + render: (text, record) => ( + {record.alt_transport} + ) + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filters: + (scheduledOutToday && + scheduledOutToday + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s] + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.status), + render: (text, record) => {record.status} + }, + { + title: t("jobs.fields.lab"), + dataIndex: "joblines_body", + key: "joblines_body", + sorter: (a, b) => a.joblines_body - b.joblines_body, + sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order, + align: "right", + render: (text, record) => ( + {record.joblines_body.toFixed(1)} + ) + }, + { + title: t("jobs.fields.lar"), + dataIndex: "joblines_ref", + key: "joblines_ref", + sorter: (a, b) => a.joblines_ref - b.joblines_ref, + sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, + align: "right", + render: (text, record) => ( + {record.joblines_ref.toFixed(1)} + ) + } + ]; - const columns = [ - { - title: t("jobs.fields.scheduled_completion"), - dataIndex: "scheduled_completion", - key: "scheduled_completion", - ellipsis: true, - sorter: (a, b) => - dateSort(a.scheduled_completion, b.scheduled_completion), - sortOrder: - state.sortedInfo.columnKey === "scheduled_completion" && - state.sortedInfo.order, - render: (text, record) => ( - {record.scheduled_completion} - ), - }, - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( - e.stopPropagation()} - > - - {record.ro_number || t("general.labels.na")} - {record.production_vars && record.production_vars.alert ? ( - - ) : null} - {record.suspended && ( - - )} - {record.iouparent && ( - - - - )} - - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => - alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - return record.ownerid ? ( - e.stopPropagation()} - > - - - ) : ( - - + const columns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, + render: (text, record) => {record.scheduled_completion} + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()}> + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && } + {record.iouparent && ( + + + + )} + + + ) + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()}> + + + ) : ( + + - ); - }, - }, - { - title: t("dashboard.labels.phone"), - dataIndex: "ownr_ph", - key: "ownr_ph", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - + ); + } + }, + { + title: t("dashboard.labels.phone"), + dataIndex: "ownr_ph", + key: "ownr_ph", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + - - ), - }, - { - title: t("jobs.fields.ownr_ea"), - dataIndex: "ownr_ea", - key: "ownr_ea", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - {record.ownr_ea} - ), - }, - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - sorter: (a, b) => - alphaSort( - `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ - a.v_model_desc || "" - }`, - `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` - ), - sortOrder: - state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { - return record.vehicleid ? ( - e.stopPropagation()} - > - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("jobs.fields.ins_co_nm"), - dataIndex: "ins_co_nm", - key: "ins_co_nm", - ellipsis: true, - responsive: ["md"], - sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), - sortOrder: - state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, - filters: - (scheduledOutToday && - scheduledOutToday - .map((j) => j.ins_co_nm) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Ins. Co.*", - value: [s], - }; - }) - .sort((a, b) => alphaSort(a.text, b.text))) || - [], - onFilter: (value, record) => value.includes(record.ins_co_nm), - }, - { - title: t("appointments.fields.alt_transport"), - dataIndex: "alt_transport", - key: "alt_transport", - ellipsis: true, - sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), - sortOrder: - state.sortedInfo.columnKey === "alt_transport" && - state.sortedInfo.order, - filters: - (scheduledOutToday && - scheduledOutToday - .map((j) => j.alt_transport) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Alt. Transport*", - value: [s], - }; - }) - .sort((a, b) => alphaSort(a.text, b.text))) || - [], - onFilter: (value, record) => value.includes(record.alt_transport),}, - ]; + + + ) + }, + { + title: t("jobs.fields.ownr_ea"), + dataIndex: "ownr_ea", + key: "ownr_ea", + ellipsis: true, + responsive: ["md"], + render: (text, record) => {record.ownr_ea} + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()}> + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ); + } + }, + { + title: t("jobs.fields.ins_co_nm"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + responsive: ["md"], + sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, + filters: + (scheduledOutToday && + scheduledOutToday + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s] + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm) + }, + { + title: t("appointments.fields.alt_transport"), + dataIndex: "alt_transport", + key: "alt_transport", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order, + filters: + (scheduledOutToday && + scheduledOutToday + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s] + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport) + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - return ( - - {t("general.labels.tvmode")} - setIsTvModeScheduledOut(!isTvModeScheduledOut)} - defaultChecked={isTvModeScheduledOut} - /> - - }{...cardProps} - > - - - - - ); + return ( + + {t("general.labels.tvmode")} + setIsTvModeScheduledOut(!isTvModeScheduledOut)} + defaultChecked={isTvModeScheduledOut} + /> + + } + {...cardProps} + > + + + + + ); } export const DashboardScheduledOutTodayGql = ` diff --git a/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx b/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx index 5e563a475..4f3bf8599 100644 --- a/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx +++ b/client/src/components/dashboard-components/total-production-dollars/total-production-dollars.component.jsx @@ -1,27 +1,23 @@ -import {Card, Statistic} from "antd"; +import { Card, Statistic } from "antd"; import Dinero from "dinero.js"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import DashboardRefreshRequired from "../refresh-required.component"; -export default function DashboardTotalProductionDollars({ - data, - ...cardProps - }) { - const {t} = useTranslation(); - if (!data) return null; - if (!data.production_jobs) return ; - const dollars = - data.production_jobs && - data.production_jobs.reduce( - (acc, val) => - acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)), - Dinero() - ); - - return ( - - - +export default function DashboardTotalProductionDollars({ data, ...cardProps }) { + const { t } = useTranslation(); + if (!data) return null; + if (!data.production_jobs) return ; + const dollars = + data.production_jobs && + data.production_jobs.reduce( + (acc, val) => acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)), + Dinero() ); + + return ( + + + + ); } diff --git a/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx b/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx index 3d43bb1fb..ffd7068db 100644 --- a/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx +++ b/client/src/components/dashboard-components/total-production-hours/total-production-hours.component.jsx @@ -1,63 +1,47 @@ -import {Card, Space, Statistic} from "antd"; +import { Card, Space, Statistic } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../../redux/user/user.selectors"; import DashboardRefreshRequired from "../refresh-required.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({}); -export default connect( - mapStateToProps, - mapDispatchToProps -)(DashboardTotalProductionHours); +export default connect(mapStateToProps, mapDispatchToProps)(DashboardTotalProductionHours); -export function DashboardTotalProductionHours({ - bodyshop, - data, - ...cardProps - }) { - const {t} = useTranslation(); - if (!data) return null; - if (!data.production_jobs) return ; - const hours = - data.production_jobs && - data.production_jobs.reduce( - (acc, val) => { - return { - body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs, - ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs, - total: - acc.total + - val.labhrs.aggregate.sum.mod_lb_hrs + - val.larhrs.aggregate.sum.mod_lb_hrs, - }; - }, - {body: 0, ref: 0, total: 0} - ); - const aboveTargetHours = hours.total >= bodyshop.prodtargethrs; - return ( - - - - - - - +export function DashboardTotalProductionHours({ bodyshop, data, ...cardProps }) { + const { t } = useTranslation(); + if (!data) return null; + if (!data.production_jobs) return ; + const hours = + data.production_jobs && + data.production_jobs.reduce( + (acc, val) => { + return { + body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs, + ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs, + total: acc.total + val.labhrs.aggregate.sum.mod_lb_hrs + val.larhrs.aggregate.sum.mod_lb_hrs + }; + }, + { body: 0, ref: 0, total: 0 } ); + const aboveTargetHours = hours.total >= bodyshop.prodtargethrs; + return ( + + + + + + + + ); } export const DashboardTotalProductionHoursGql = ``; diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 78466ad0a..614dbb90f 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -1,325 +1,316 @@ -import Icon, {SyncOutlined} from "@ant-design/icons"; -import {gql, useMutation, useQuery} from "@apollo/client"; -import {Button, Dropdown, notification, Space} from "antd"; -import {PageHeader} from "@ant-design/pro-layout"; +import Icon, { SyncOutlined } from "@ant-design/icons"; +import { gql, useMutation, useQuery } from "@apollo/client"; +import { Button, Dropdown, notification, Space } from "antd"; +import { PageHeader } from "@ant-design/pro-layout"; import i18next from "i18next"; import _ from "lodash"; import dayjs from "../../utils/day"; -import React, {useState} from "react"; -import {Responsive, WidthProvider} from "react-grid-layout"; -import {useTranslation} from "react-i18next"; -import {MdClose} from "react-icons/md"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { Responsive, WidthProvider } from "react-grid-layout"; +import { useTranslation } from "react-i18next"; +import { MdClose } from "react-icons/md"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import DashboardMonthlyEmployeeEfficiency, { - DashboardMonthlyEmployeeEfficiencyGql, + DashboardMonthlyEmployeeEfficiencyGql } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component"; import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component"; import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component"; import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component"; import DashboardMonthlyRevenueGraph, { - DashboardMonthlyRevenueGraphGql, + DashboardMonthlyRevenueGraphGql } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; import DashboardProjectedMonthlySales, { - DashboardProjectedMonthlySalesGql, + DashboardProjectedMonthlySalesGql } from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; -import DashboardTotalProductionDollars - from "../dashboard-components/total-production-dollars/total-production-dollars.component"; +import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component"; import DashboardTotalProductionHours, { - DashboardTotalProductionHoursGql, + DashboardTotalProductionHoursGql } from "../dashboard-components/total-production-hours/total-production-hours.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; //Combination of the following: // /node_modules/react-grid-layout/css/styles.css // /node_modules/react-resizable/css/styles.css import DashboardScheduledInToday, { - DashboardScheduledInTodayGql, + DashboardScheduledInTodayGql } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component"; import DashboardScheduledOutToday, { - DashboardScheduledOutTodayGql, + DashboardScheduledOutTodayGql } from "../dashboard-components/scheduled-out-today/scheduled-out-today.component"; import JobLifecycleDashboardComponent, { - JobLifecycleDashboardGQL + JobLifecycleDashboardGQL } from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component"; import "./dashboard-grid.styles.scss"; -import {GenerateDashboardData} from "./dashboard-grid.utils"; +import { GenerateDashboardData } from "./dashboard-grid.utils"; const ResponsiveReactGridLayout = WidthProvider(Responsive); const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function DashboardGridComponent({currentUser, bodyshop}) { - const {t} = useTranslation(); - const [state, setState] = useState({ - ...(bodyshop.associations[0].user.dashboardlayout - ? bodyshop.associations[0].user.dashboardlayout - : {items: [], layout: {}, layouts: []}), +export function DashboardGridComponent({ currentUser, bodyshop }) { + const { t } = useTranslation(); + const [state, setState] = useState({ + ...(bodyshop.associations[0].user.dashboardlayout + ? bodyshop.associations[0].user.dashboardlayout + : { items: [], layout: {}, layouts: [] }) + }); + + const { loading, error, data, refetch } = useQuery(createDashboardQuery(state), { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + + const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); + + const handleLayoutChange = async (layout, layouts) => { + logImEXEvent("dashboard_change_layout"); + + setState({ ...state, layout, layouts }); + + const result = await updateLayout({ + variables: { + email: currentUser.email, + layout: { ...state, layout, layouts } + } }); + if (!!result.errors) { + notification["error"]({ + message: t("dashboard.errors.updatinglayout", { + message: JSON.stringify(result.errors) + }) + }); + } + }; + const handleRemoveComponent = (key) => { + logImEXEvent("dashboard_remove_component", { name: key }); + const idxToRemove = state.items.findIndex((i) => i.i === key); - const {loading, error, data, refetch} = useQuery( - createDashboardQuery(state), - {fetchPolicy: "network-only", nextFetchPolicy: "network-only"} - ); + const items = _.cloneDeep(state.items); - const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); + items.splice(idxToRemove, 1); + setState({ ...state, items }); + }; - const handleLayoutChange = async (layout, layouts) => { - logImEXEvent("dashboard_change_layout"); - - setState({...state, layout, layouts}); - - const result = await updateLayout({ - variables: { - email: currentUser.email, - layout: {...state, layout, layouts}, - }, - }); - if (!!result.errors) { - notification["error"]({ - message: t("dashboard.errors.updatinglayout", { - message: JSON.stringify(result.errors), - }), - }); + const handleAddComponent = (e) => { + logImEXEvent("dashboard_add_component", { name: e }); + setState({ + ...state, + items: [ + ...state.items, + { + i: e.key, + x: (state.items.length * 2) % (state.cols || 12), + y: 99, // puts it at the bottom + w: componentList[e.key].w || 2, + h: componentList[e.key].h || 2 } - }; - const handleRemoveComponent = (key) => { - logImEXEvent("dashboard_remove_component", {name: key}); - const idxToRemove = state.items.findIndex((i) => i.i === key); + ] + }); + }; - const items = _.cloneDeep(state.items); + const dashboarddata = React.useMemo(() => GenerateDashboardData(data), [data]); + const existingLayoutKeys = state.items.map((i) => i.i); - items.splice(idxToRemove, 1); - setState({...state, items}); - }; + const menuItems = Object.keys(componentList).map((key) => ({ + key: key, + label: componentList[key].label, + value: key, + disabled: existingLayoutKeys.includes(key) + })); - const handleAddComponent = (e) => { - logImEXEvent("dashboard_add_component", {name: e}); - setState({ - ...state, - items: [ - ...state.items, - { - i: e.key, - x: (state.items.length * 2) % (state.cols || 12), - y: 99, // puts it at the bottom - w: componentList[e.key].w || 2, - h: componentList[e.key].h || 2, - }, - ], - }); - }; + const menu = { items: menuItems, onClick: handleAddComponent }; - const dashboarddata = React.useMemo( - () => GenerateDashboardData(data), - [data] - ); - const existingLayoutKeys = state.items.map((i) => i.i); + if (error) return ; - const menuItems = Object.keys(componentList).map((key) => ({ - key: key, - label: componentList[key].label, - value: key, - disabled: existingLayoutKeys.includes(key), - })); + return ( + + + refetch()}> + + + + {t("dashboard.actions.addcomponent")} + + + } + /> - const menu = {items: menuItems, onClick: handleAddComponent}; - - if (error) return ; - - return ( - - - refetch()}> - - - - {t("dashboard.actions.addcomponent")} - - - } - /> - - + {state.items.map((item, index) => { + const TheComponent = componentList[item.i].component; + return ( + - {state.items.map((item, index) => { - const TheComponent = componentList[item.i].component; - return ( - - - handleRemoveComponent(item.i)} - /> - - - - ); - })} - - - ); + + handleRemoveComponent(item.i)} + /> + + + + ); + })} + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(DashboardGridComponent); +export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent); const componentList = { - ProductionDollars: { - label: i18next.t("dashboard.titles.productiondollars"), - component: DashboardTotalProductionDollars, - gqlFragment: null, - w: 1, - h: 1, - minW: 2, - minH: 1, - }, - ProductionHours: { - label: i18next.t("dashboard.titles.productionhours"), - component: DashboardTotalProductionHours, - gqlFragment: DashboardTotalProductionHoursGql, - w: 3, - h: 1, - minW: 3, - minH: 1, - }, - ProjectedMonthlySales: { - label: i18next.t("dashboard.titles.projectedmonthlysales"), - component: DashboardProjectedMonthlySales, - gqlFragment: DashboardProjectedMonthlySalesGql, - w: 2, - h: 1, - minW: 2, - minH: 1, - }, - MonthlyRevenueGraph: { - label: i18next.t("dashboard.titles.monthlyrevenuegraph"), - component: DashboardMonthlyRevenueGraph, - gqlFragment: DashboardMonthlyRevenueGraphGql, - w: 4, - h: 2, - minW: 4, - minH: 2, - }, - MonthlyJobCosting: { - label: i18next.t("dashboard.titles.monthlyjobcosting"), - component: DashboardMonthlyJobCosting, - gqlFragment: null, - minW: 6, - minH: 3, - w: 6, - h: 3, - }, - MonthlyPartsSales: { - label: i18next.t("dashboard.titles.monthlypartssales"), - component: DashboardMonthlyPartsSales, - gqlFragment: null, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - MonthlyLaborSales: { - label: i18next.t("dashboard.titles.monthlylaborsales"), - component: DashboardMonthlyLaborSales, - gqlFragment: null, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - // Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings - MonthlyEmployeeEfficency: { - label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), - component: DashboardMonthlyEmployeeEfficiency, - gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - ScheduleInToday: { - label: i18next.t("dashboard.titles.scheduledintoday"), - component: DashboardScheduledInToday, - gqlFragment: DashboardScheduledInTodayGql, - minW: 6, - minH: 2, - w: 10, - h: 3, - }, - ScheduleOutToday: { - label: i18next.t("dashboard.titles.scheduledouttoday"), - component: DashboardScheduledOutToday, - gqlFragment: DashboardScheduledOutTodayGql, - minW: 6, - minH: 2, - w: 10, - h: 3, - }, - JobLifecycle: { - label: i18next.t("dashboard.titles.joblifecycle"), - component: JobLifecycleDashboardComponent, - gqlFragment: JobLifecycleDashboardGQL, - minW: 6, - minH: 3, - w: 6, - h: 3, - }, + ProductionDollars: { + label: i18next.t("dashboard.titles.productiondollars"), + component: DashboardTotalProductionDollars, + gqlFragment: null, + w: 1, + h: 1, + minW: 2, + minH: 1 + }, + ProductionHours: { + label: i18next.t("dashboard.titles.productionhours"), + component: DashboardTotalProductionHours, + gqlFragment: DashboardTotalProductionHoursGql, + w: 3, + h: 1, + minW: 3, + minH: 1 + }, + ProjectedMonthlySales: { + label: i18next.t("dashboard.titles.projectedmonthlysales"), + component: DashboardProjectedMonthlySales, + gqlFragment: DashboardProjectedMonthlySalesGql, + w: 2, + h: 1, + minW: 2, + minH: 1 + }, + MonthlyRevenueGraph: { + label: i18next.t("dashboard.titles.monthlyrevenuegraph"), + component: DashboardMonthlyRevenueGraph, + gqlFragment: DashboardMonthlyRevenueGraphGql, + w: 4, + h: 2, + minW: 4, + minH: 2 + }, + MonthlyJobCosting: { + label: i18next.t("dashboard.titles.monthlyjobcosting"), + component: DashboardMonthlyJobCosting, + gqlFragment: null, + minW: 6, + minH: 3, + w: 6, + h: 3 + }, + MonthlyPartsSales: { + label: i18next.t("dashboard.titles.monthlypartssales"), + component: DashboardMonthlyPartsSales, + gqlFragment: null, + minW: 2, + minH: 2, + w: 2, + h: 2 + }, + MonthlyLaborSales: { + label: i18next.t("dashboard.titles.monthlylaborsales"), + component: DashboardMonthlyLaborSales, + gqlFragment: null, + minW: 2, + minH: 2, + w: 2, + h: 2 + }, + // Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings + MonthlyEmployeeEfficency: { + label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), + component: DashboardMonthlyEmployeeEfficiency, + gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, + minW: 2, + minH: 2, + w: 2, + h: 2 + }, + ScheduleInToday: { + label: i18next.t("dashboard.titles.scheduledintoday"), + component: DashboardScheduledInToday, + gqlFragment: DashboardScheduledInTodayGql, + minW: 6, + minH: 2, + w: 10, + h: 3 + }, + ScheduleOutToday: { + label: i18next.t("dashboard.titles.scheduledouttoday"), + component: DashboardScheduledOutToday, + gqlFragment: DashboardScheduledOutTodayGql, + minW: 6, + minH: 2, + w: 10, + h: 3 + }, + JobLifecycle: { + label: i18next.t("dashboard.titles.joblifecycle"), + component: JobLifecycleDashboardComponent, + gqlFragment: JobLifecycleDashboardGQL, + minW: 6, + minH: 3, + w: 6, + h: 3 + } }; const createDashboardQuery = (state) => { - const componentBasedAdditions = - state && - Array.isArray(state.layout) && - state.layout - .map((item, index) => componentList[item.i].gqlFragment || "") - .join(""); - return gql` + const componentBasedAdditions = + state && + Array.isArray(state.layout) && + state.layout.map((item, index) => componentList[item.i].gqlFragment || "").join(""); + return gql` query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} monthly_sales: jobs(where: {_and: [ { voided: {_eq: false}}, {date_invoiced: {_gte: "${dayjs() - .startOf("month") - .startOf("day") - .toISOString()}"}}, {date_invoiced: {_lte: "${dayjs() - .endOf("month") - .endOf("day") - .toISOString()}"}}]}) { + .startOf("month") + .startOf("day") + .toISOString()}"}}, {date_invoiced: {_lte: "${dayjs() + .endOf("month") + .endOf("day") + .toISOString()}"}}]}) { id ro_number date_invoiced diff --git a/client/src/components/dashboard-grid/dashboard-grid.utils.js b/client/src/components/dashboard-grid/dashboard-grid.utils.js index a5fde93ba..d7215d1b4 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.utils.js +++ b/client/src/components/dashboard-grid/dashboard-grid.utils.js @@ -1,3 +1,3 @@ export function GenerateDashboardData(data) { - return data; + return data; } diff --git a/client/src/components/data-label/data-label.component.jsx b/client/src/components/data-label/data-label.component.jsx index d95dbe697..f3a0880de 100644 --- a/client/src/components/data-label/data-label.component.jsx +++ b/client/src/components/data-label/data-label.component.jsx @@ -1,46 +1,42 @@ -import {Typography} from "antd"; +import { Typography } from "antd"; import React from "react"; export default function DataLabel({ - label, - hideIfNull, - children, - vertical, - open = true, - valueStyle = {}, - valueClassName, - onValueClick, - ...props - }) { - if (!open || (hideIfNull && !!!children)) return null; + label, + hideIfNull, + children, + vertical, + open = true, + valueStyle = {}, + valueClassName, + onValueClick, + ...props +}) { + if (!open || (hideIfNull && !!!children)) return null; - return ( - - - {`${label}:`} - - - {typeof children === "string" ? ( - {children} - ) : ( - children - )} - - - ); + return ( + + + {`${label}:`} + + + {typeof children === "string" ? {children} : children} + + + ); } diff --git a/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx index 369dc18f4..b4fc66577 100644 --- a/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx +++ b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx @@ -1,153 +1,142 @@ -import {SyncOutlined} from "@ant-design/icons"; -import {Button, Card, Form, Input, Table} from "antd"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {pageLimit} from "../../utils/config"; +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Form, Input, Table } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(DmsAllocationsSummaryAp); +export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummaryAp); -export function DmsAllocationsSummaryAp({socket, bodyshop, billids, title}) { - const {t} = useTranslation(); - const [allocationsSummary, setAllocationsSummary] = useState([]); +export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) { + const { t } = useTranslation(); + const [allocationsSummary, setAllocationsSummary] = useState([]); - useEffect(() => { - socket.on("ap-export-success", (billid) => { - setAllocationsSummary((allocationsSummary) => - allocationsSummary.map((a) => { - if (a.billid !== billid) return a; - return {...a, status: "Successful"}; - }) - ); - }); - socket.on("ap-export-failure", ({billid, error}) => { - allocationsSummary.map((a) => { - if (a.billid !== billid) return a; - return {...a, status: error}; - }); - }); + useEffect(() => { + socket.on("ap-export-success", (billid) => { + setAllocationsSummary((allocationsSummary) => + allocationsSummary.map((a) => { + if (a.billid !== billid) return a; + return { ...a, status: "Successful" }; + }) + ); + }); + socket.on("ap-export-failure", ({ billid, error }) => { + allocationsSummary.map((a) => { + if (a.billid !== billid) return a; + return { ...a, status: error }; + }); + }); - if (socket.disconnected) socket.connect(); - return () => { - socket.removeListener("ap-export-success"); - socket.removeListener("ap-export-failure"); - //socket.disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (socket.connected) { - socket.emit("pbs-calculate-allocations-ap", billids, (ack) => { - setAllocationsSummary(ack); - - socket.allocationsSummary = ack; - }); - } - }, [socket, socket.connected, billids]); - console.log(allocationsSummary); - const columns = [ - { - title: t("general.labels.status"), - dataIndex: "status", - key: "status", - }, - { - title: t("bills.fields.invoice_number"), - dataIndex: ["Posting", "Reference"], - key: "reference", - }, - - { - title: t("jobs.fields.dms.lines"), - dataIndex: "Lines", - key: "Lines", - render: (text, record) => ( - - - {t("bills.fields.invoice_number")} - {t("bodyshop.fields.dms.dms_acctnumber")} - {t("jobs.fields.dms.amount")} - - {record.Posting.Lines.map((l, idx) => ( - - {l.InvoiceNumber} - {l.Account} - {l.Amount} - - ))} - - ), - }, - ]; - - const handleFinish = async (values) => { - socket.emit(`pbs-export-ap`, { - billids, - txEnvelope: values, - }); + if (socket.disconnected) socket.connect(); + return () => { + socket.removeListener("ap-export-success"); + socket.removeListener("ap-export-failure"); + //socket.disconnect(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - return ( - { - socket.emit("pbs-calculate-allocations-ap", billids, (ack) => - setAllocationsSummary(ack) - ); - }} - > - - - } + useEffect(() => { + if (socket.connected) { + socket.emit("pbs-calculate-allocations-ap", billids, (ack) => { + setAllocationsSummary(ack); + + socket.allocationsSummary = ack; + }); + } + }, [socket, socket.connected, billids]); + console.log(allocationsSummary); + const columns = [ + { + title: t("general.labels.status"), + dataIndex: "status", + key: "status" + }, + { + title: t("bills.fields.invoice_number"), + dataIndex: ["Posting", "Reference"], + key: "reference" + }, + + { + title: t("jobs.fields.dms.lines"), + dataIndex: "Lines", + key: "Lines", + render: (text, record) => ( + + + {t("bills.fields.invoice_number")} + {t("bodyshop.fields.dms.dms_acctnumber")} + {t("jobs.fields.dms.amount")} + + {record.Posting.Lines.map((l, idx) => ( + + {l.InvoiceNumber} + {l.Account} + {l.Amount} + + ))} + + ) + } + ]; + + const handleFinish = async (values) => { + socket.emit(`pbs-export-ap`, { + billids, + txEnvelope: values + }); + }; + + return ( + { + socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack)); + }} > - `${record.InvoiceNumber}${record.Account}`} - dataSource={allocationsSummary} - locale={{emptyText: t("dms.labels.refreshallocations")}} - /> - - - - - - {t("jobs.actions.dms.post")} - - - - ); + + + } + > + `${record.InvoiceNumber}${record.Account}`} + dataSource={allocationsSummary} + locale={{ emptyText: t("dms.labels.refreshallocations") }} + /> + + + + + + {t("jobs.actions.dms.post")} + + + + ); } diff --git a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx index 68c65aacf..3551cc6e3 100644 --- a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx +++ b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx @@ -1,142 +1,130 @@ -import {Alert, Button, Card, Table, Typography} from "antd"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { Alert, Button, Card, Table, Typography } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import Dinero from "dinero.js"; -import {SyncOutlined} from "@ant-design/icons"; -import {pageLimit} from "../../utils/config"; +import { SyncOutlined } from "@ant-design/icons"; +import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(DmsAllocationsSummary); +export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummary); -export function DmsAllocationsSummary({socket, bodyshop, jobId, title}) { - const {t} = useTranslation(); - const [allocationsSummary, setAllocationsSummary] = useState([]); +export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) { + const { t } = useTranslation(); + const [allocationsSummary, setAllocationsSummary] = useState([]); - useEffect(() => { - if (socket.connected) { - socket.emit("cdk-calculate-allocations", jobId, (ack) => { - setAllocationsSummary(ack); - socket.allocationsSummary = ack; - }); - } - }, [socket, socket.connected, jobId]); + useEffect(() => { + if (socket.connected) { + socket.emit("cdk-calculate-allocations", jobId, (ack) => { + setAllocationsSummary(ack); + socket.allocationsSummary = ack; + }); + } + }, [socket, socket.connected, jobId]); - const columns = [ - { - title: t("jobs.fields.dms.center"), - dataIndex: "center", - key: "center", - }, - { - title: t("jobs.fields.dms.sale"), - dataIndex: "sale", - key: "sale", - render: (text, record) => Dinero(record.sale).toFormat(), - }, - { - title: t("jobs.fields.dms.cost"), - dataIndex: "cost", - key: "cost", - render: (text, record) => Dinero(record.cost).toFormat(), - }, - { - title: t("jobs.fields.dms.sale_dms_acctnumber"), - dataIndex: "sale_dms_acctnumber", - key: "sale_dms_acctnumber", - render: (text, record) => - record.profitCenter && record.profitCenter.dms_acctnumber, - }, - { - title: t("jobs.fields.dms.cost_dms_acctnumber"), - dataIndex: "cost_dms_acctnumber", - key: "cost_dms_acctnumber", - render: (text, record) => - record.costCenter && record.costCenter.dms_acctnumber, - }, - { - title: t("jobs.fields.dms.dms_wip_acctnumber"), - dataIndex: "dms_wip_acctnumber", - key: "dms_wip_acctnumber", - render: (text, record) => - record.costCenter && record.costCenter.dms_wip_acctnumber, - }, - ]; + const columns = [ + { + title: t("jobs.fields.dms.center"), + dataIndex: "center", + key: "center" + }, + { + title: t("jobs.fields.dms.sale"), + dataIndex: "sale", + key: "sale", + render: (text, record) => Dinero(record.sale).toFormat() + }, + { + title: t("jobs.fields.dms.cost"), + dataIndex: "cost", + key: "cost", + render: (text, record) => Dinero(record.cost).toFormat() + }, + { + title: t("jobs.fields.dms.sale_dms_acctnumber"), + dataIndex: "sale_dms_acctnumber", + key: "sale_dms_acctnumber", + render: (text, record) => record.profitCenter && record.profitCenter.dms_acctnumber + }, + { + title: t("jobs.fields.dms.cost_dms_acctnumber"), + dataIndex: "cost_dms_acctnumber", + key: "cost_dms_acctnumber", + render: (text, record) => record.costCenter && record.costCenter.dms_acctnumber + }, + { + title: t("jobs.fields.dms.dms_wip_acctnumber"), + dataIndex: "dms_wip_acctnumber", + key: "dms_wip_acctnumber", + render: (text, record) => record.costCenter && record.costCenter.dms_wip_acctnumber + } + ]; - return ( - { - socket.emit("cdk-calculate-allocations", jobId, (ack) => - setAllocationsSummary(ack) - ); - }} - > - - - } + return ( + { + socket.emit("cdk-calculate-allocations", jobId, (ack) => setAllocationsSummary(ack)); + }} > - {bodyshop.pbs_configuration?.disablebillwip && ( - - )} - { - const totals = - allocationsSummary && - allocationsSummary.reduce( - (acc, val) => { - return { - totalSale: acc.totalSale.add(Dinero(val.sale)), - totalCost: acc.totalCost.add(Dinero(val.cost)), - }; - }, - { - totalSale: Dinero(), - totalCost: Dinero(), - } - ); + + + } + > + {bodyshop.pbs_configuration?.disablebillwip && ( + + )} + { + const totals = + allocationsSummary && + allocationsSummary.reduce( + (acc, val) => { + return { + totalSale: acc.totalSale.add(Dinero(val.sale)), + totalCost: acc.totalCost.add(Dinero(val.cost)) + }; + }, + { + totalSale: Dinero(), + totalCost: Dinero() + } + ); - return ( - - - - {t("general.labels.totals")} - - - - {totals && totals.totalSale.toFormat()} - - - { - // totals.totalCost.toFormat() - } - - - - - ); - }} - /> - - ); + return ( + + + {t("general.labels.totals")} + + {totals && totals.totalSale.toFormat()} + + { + // totals.totalCost.toFormat() + } + + + + + ); + }} + /> + + ); } diff --git a/client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx b/client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx index b0df8abb5..27a63c850 100644 --- a/client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx +++ b/client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx @@ -1,105 +1,104 @@ -import {useLazyQuery} from "@apollo/client"; -import {Button, Input, Modal, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {SEARCH_DMS_VEHICLES} from "../../graphql/dms.queries"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useLazyQuery } from "@apollo/client"; +import { Button, Input, Modal, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { SEARCH_DMS_VEHICLES } from "../../graphql/dms.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles); -export function DmsCdkVehicles({bodyshop, form, socket, job}) { - const [open, setOpen] = useState(false); - const [selectedModel, setSelectedModel] = useState(null); - const {t} = useTranslation(); +export function DmsCdkVehicles({ bodyshop, form, socket, job }) { + const [open, setOpen] = useState(false); + const [selectedModel, setSelectedModel] = useState(null); + const { t } = useTranslation(); - const [callSearch, {loading, error, data}] = - useLazyQuery(SEARCH_DMS_VEHICLES); - const columns = [ - { - title: t("vehicles.fields.v_make_desc"), - dataIndex: "make", - key: "make", - }, - { - title: t("vehicles.fields.v_model_desc"), - dataIndex: "model", - key: "model", - }, - { - title: t("jobs.fields.dms.dms_make"), - dataIndex: "makecode", - key: "makecode", - }, - { - title: t("jobs.fields.dms.dms_model"), - dataIndex: "modelcode", - key: "modelcode", - }, - ]; + const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_DMS_VEHICLES); + const columns = [ + { + title: t("vehicles.fields.v_make_desc"), + dataIndex: "make", + key: "make" + }, + { + title: t("vehicles.fields.v_model_desc"), + dataIndex: "model", + key: "model" + }, + { + title: t("jobs.fields.dms.dms_make"), + dataIndex: "makecode", + key: "makecode" + }, + { + title: t("jobs.fields.dms.dms_model"), + dataIndex: "modelcode", + key: "modelcode" + } + ]; - return ( - <> - setOpen(false)} - onOk={() => { - form.setFieldsValue({ - dms_make: selectedModel.makecode, - dms_model: selectedModel.modelcode, - }); - setOpen(false); - }} - > - {error && } - ( - callSearch({variables: {search: val}})} - placeholder={t("general.labels.search")} - /> - )} - columns={columns} - loading={loading} - rowKey="id" - dataSource={data ? data.search_dms_vehicles : []} - onRow={(record) => { - return { - onClick: () => setSelectedModel(record), - }; - }} - rowSelection={{ - onSelect: (record) => { - setSelectedModel(record); - }, + return ( + <> + setOpen(false)} + onOk={() => { + form.setFieldsValue({ + dms_make: selectedModel.makecode, + dms_model: selectedModel.modelcode + }); + setOpen(false); + }} + > + {error && } + ( + callSearch({ variables: { search: val } })} + placeholder={t("general.labels.search")} + /> + )} + columns={columns} + loading={loading} + rowKey="id" + dataSource={data ? data.search_dms_vehicles : []} + onRow={(record) => { + return { + onClick: () => setSelectedModel(record) + }; + }} + rowSelection={{ + onSelect: (record) => { + setSelectedModel(record); + }, - type: "radio", - selectedRowKeys: [selectedModel && selectedModel.id], - }} - /> - - { - setOpen(true); - callSearch({ - variables: { - search: job && job.v_model_desc && job.v_model_desc.substr(0, 3), - }, - }); - }} - > - {t("jobs.actions.dms.findmakemodelcode")} - - > - ); + type: "radio", + selectedRowKeys: [selectedModel && selectedModel.id] + }} + /> + + { + setOpen(true); + callSearch({ + variables: { + search: job && job.v_model_desc && job.v_model_desc.substr(0, 3) + } + }); + }} + > + {t("jobs.actions.dms.findmakemodelcode")} + + > + ); } diff --git a/client/src/components/dms-cdk-makes/dms-cdk-makes.refetch.component.jsx b/client/src/components/dms-cdk-makes/dms-cdk-makes.refetch.component.jsx index d11aab598..ad505f9c3 100644 --- a/client/src/components/dms-cdk-makes/dms-cdk-makes.refetch.component.jsx +++ b/client/src/components/dms-cdk-makes/dms-cdk-makes.refetch.component.jsx @@ -1,37 +1,37 @@ -import {Button} from "antd"; +import { Button } from "antd"; import axios from "axios"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakesRefetch); -export function DmsCdkMakesRefetch({currentUser, bodyshop, form, socket}) { - const [loading, setLoading] = useState(false); - const {t} = useTranslation(); +export function DmsCdkMakesRefetch({ currentUser, bodyshop, form, socket }) { + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); - if (!currentUser.email.includes("@imex.")) return null; + if (!currentUser.email.includes("@imex.")) return null; - const handleRefetch = async () => { - setLoading(true); - await axios.post("/cdk/getvehicles", { - cdk_dealerid: bodyshop.cdk_dealerid, - bodyshopid: bodyshop.id, - }); + const handleRefetch = async () => { + setLoading(true); + await axios.post("/cdk/getvehicles", { + cdk_dealerid: bodyshop.cdk_dealerid, + bodyshopid: bodyshop.id + }); - setLoading(false); - }; - return ( - - {t("jobs.actions.dms.refetchmakesmodels")} - - ); + setLoading(false); + }; + return ( + + {t("jobs.actions.dms.refetchmakesmodels")} + + ); } diff --git a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx index 88df58895..ec82d36d3 100644 --- a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx +++ b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx @@ -1,161 +1,139 @@ -import {Button, Checkbox, Col, Table} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {socket} from "../../pages/dms/dms.container"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {alphaSort} from "../../utils/sorters"; +import { Button, Checkbox, Col, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { socket } from "../../pages/dms/dms.container"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { alphaSort } from "../../utils/sorters"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(DmsCustomerSelector); +export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector); -export function DmsCustomerSelector({bodyshop}) { - const {t} = useTranslation(); - const [customerList, setcustomerList] = useState([]); - const [open, setOpen] = useState(false); - const [selectedCustomer, setSelectedCustomer] = useState(null); - const [dmsType, setDmsType] = useState("cdk"); +export function DmsCustomerSelector({ bodyshop }) { + const { t } = useTranslation(); + const [customerList, setcustomerList] = useState([]); + const [open, setOpen] = useState(false); + const [selectedCustomer, setSelectedCustomer] = useState(null); + const [dmsType, setDmsType] = useState("cdk"); - socket.on("cdk-select-customer", (customerList, callback) => { - setOpen(true); - setDmsType("cdk"); - setcustomerList(customerList); - }); - socket.on("pbs-select-customer", (customerList, callback) => { - setOpen(true); - setDmsType("pbs"); - setcustomerList(customerList); - }); + socket.on("cdk-select-customer", (customerList, callback) => { + setOpen(true); + setDmsType("cdk"); + setcustomerList(customerList); + }); + socket.on("pbs-select-customer", (customerList, callback) => { + setOpen(true); + setDmsType("pbs"); + setcustomerList(customerList); + }); - const onUseSelected = () => { - setOpen(false); - socket.emit(`${dmsType}-selected-customer`, selectedCustomer); - setSelectedCustomer(null); - }; + const onUseSelected = () => { + setOpen(false); + socket.emit(`${dmsType}-selected-customer`, selectedCustomer); + setSelectedCustomer(null); + }; - const onUseGeneric = () => { - setOpen(false); - socket.emit( - `${dmsType}-selected-customer`, - bodyshop.cdk_configuration.generic_customer_number - ); - setSelectedCustomer(null); - }; + const onUseGeneric = () => { + setOpen(false); + socket.emit(`${dmsType}-selected-customer`, bodyshop.cdk_configuration.generic_customer_number); + setSelectedCustomer(null); + }; - const onCreateNew = () => { - setOpen(false); - socket.emit(`${dmsType}-selected-customer`, null); - setSelectedCustomer(null); - }; + const onCreateNew = () => { + setOpen(false); + socket.emit(`${dmsType}-selected-customer`, null); + setSelectedCustomer(null); + }; - const cdkColumns = [ - { - title: t("jobs.fields.dms.id"), - dataIndex: ["id", "value"], - key: "id", - }, - { - title: t("jobs.fields.dms.vinowner"), - dataIndex: "vinOwner", - key: "vinOwner", - render: (text, record) => , - }, - { - title: t("jobs.fields.dms.name1"), - dataIndex: ["name1", "fullName"], - key: "name1", - sorter: (a, b) => - alphaSort(a.name1 && a.name1.fullName, b.name1 && b.name1.fullName), - }, + const cdkColumns = [ + { + title: t("jobs.fields.dms.id"), + dataIndex: ["id", "value"], + key: "id" + }, + { + title: t("jobs.fields.dms.vinowner"), + dataIndex: "vinOwner", + key: "vinOwner", + render: (text, record) => + }, + { + title: t("jobs.fields.dms.name1"), + dataIndex: ["name1", "fullName"], + key: "name1", + sorter: (a, b) => alphaSort(a.name1 && a.name1.fullName, b.name1 && b.name1.fullName) + }, - { - title: t("jobs.fields.dms.address"), - //dataIndex: ["name2", "fullName"], - key: "address", - render: (record, value) => - `${ - record.address && - record.address.addressLine && - record.address.addressLine[0] - }, ${record.address && record.address.city} ${ - record.address && record.address.stateOrProvince - } ${record.address && record.address.postalCode}`, - }, - ]; + { + title: t("jobs.fields.dms.address"), + //dataIndex: ["name2", "fullName"], + key: "address", + render: (record, value) => + `${ + record.address && record.address.addressLine && record.address.addressLine[0] + }, ${record.address && record.address.city} ${ + record.address && record.address.stateOrProvince + } ${record.address && record.address.postalCode}` + } + ]; - const pbsColumns = [ - { - title: t("jobs.fields.dms.id"), - dataIndex: "ContactId", - key: "ContactId", - }, - { - title: t("jobs.fields.dms.name1"), - key: "name1", - sorter: (a, b) => alphaSort(a.LastName, b.LastName), - render: (text, record) => - `${record.FirstName || ""} ${record.LastName || ""}`, - }, + const pbsColumns = [ + { + title: t("jobs.fields.dms.id"), + dataIndex: "ContactId", + key: "ContactId" + }, + { + title: t("jobs.fields.dms.name1"), + key: "name1", + sorter: (a, b) => alphaSort(a.LastName, b.LastName), + render: (text, record) => `${record.FirstName || ""} ${record.LastName || ""}` + }, - { - title: t("jobs.fields.dms.address"), - key: "address", - render: (record, value) => - `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`, - }, - ]; + { + title: t("jobs.fields.dms.address"), + key: "address", + render: (record, value) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}` + } + ]; - if (!open) return null; - return ( - - ( - - - {t("jobs.actions.dms.useselected")} - - - {t("jobs.actions.dms.usegeneric")} - - - {t("jobs.actions.dms.createnewcustomer")} - - - )} - pagination={{position: "top"}} - columns={dmsType === "cdk" ? cdkColumns : pbsColumns} - rowKey={(record) => - dmsType === "cdk" ? record.id.value : record.ContactId - } - dataSource={customerList} - //onChange={handleTableChange} - rowSelection={{ - onSelect: (record) => { - setSelectedCustomer( - dmsType === "cdk" ? record.id.value : record.ContactId - ); - }, - type: "radio", - selectedRowKeys: [selectedCustomer], - }} - /> - - ); + if (!open) return null; + return ( + + ( + + + {t("jobs.actions.dms.useselected")} + + + {t("jobs.actions.dms.usegeneric")} + + {t("jobs.actions.dms.createnewcustomer")} + + )} + pagination={{ position: "top" }} + columns={dmsType === "cdk" ? cdkColumns : pbsColumns} + rowKey={(record) => (dmsType === "cdk" ? record.id.value : record.ContactId)} + dataSource={customerList} + //onChange={handleTableChange} + rowSelection={{ + onSelect: (record) => { + setSelectedCustomer(dmsType === "cdk" ? record.id.value : record.ContactId); + }, + type: "radio", + selectedRowKeys: [selectedCustomer] + }} + /> + + ); } diff --git a/client/src/components/dms-log-events/dms-log-events.component.jsx b/client/src/components/dms-log-events/dms-log-events.component.jsx index b372df6e2..037664900 100644 --- a/client/src/components/dms-log-events/dms-log-events.component.jsx +++ b/client/src/components/dms-log-events/dms-log-events.component.jsx @@ -1,56 +1,56 @@ -import {Divider, Space, Tag, Timeline} from "antd"; +import { Divider, Space, Tag, Timeline } from "antd"; import dayjs from "../../utils/day"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), - setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), + setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) }); export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents); -export function DmsLogEvents({socket, logs, bodyshop}) { - return ( - ({ - key: idx, - color: LogLevelHierarchy(log.level), - children: ( - - {log.level} - {dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")} - - {log.message} - - ), - }))} - /> - ); +export function DmsLogEvents({ socket, logs, bodyshop }) { + return ( + ({ + key: idx, + color: LogLevelHierarchy(log.level), + children: ( + + {log.level} + {dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")} + + {log.message} + + ) + }))} + /> + ); } function LogLevelHierarchy(level) { - switch (level) { - case "TRACE": - return "pink"; - case "DEBUG": - return "orange"; - case "INFO": - return "blue"; - case "WARNING": - return "yellow"; - case "ERROR": - return "red"; - default: - return 0; - } + switch (level) { + case "TRACE": + return "pink"; + case "DEBUG": + return "orange"; + case "INFO": + return "blue"; + case "WARNING": + return "yellow"; + case "ERROR": + return "red"; + default: + return 0; + } } diff --git a/client/src/components/dms-post-form/dms-post-form.component.jsx b/client/src/components/dms-post-form/dms-post-form.component.jsx index 654a7ec79..89738cc95 100644 --- a/client/src/components/dms-post-form/dms-post-form.component.jsx +++ b/client/src/components/dms-post-form/dms-post-form.component.jsx @@ -1,26 +1,26 @@ -import {DeleteFilled, DownOutlined} from "@ant-design/icons"; +import { DeleteFilled, DownOutlined } from "@ant-design/icons"; import { - Button, - Card, - Divider, - Dropdown, - Form, - Input, - InputNumber, - Select, - Space, - Statistic, - Switch, - Typography, + Button, + Card, + Divider, + Dropdown, + Form, + Input, + InputNumber, + Select, + Space, + Statistic, + Switch, + Typography } from "antd"; import Dinero from "dinero.js"; import dayjs from "../../utils/day"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {determineDmsType} from "../../pages/dms/dms.container"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { determineDmsType } from "../../pages/dms/dms.container"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import i18n from "../../translations/i18n"; import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component"; @@ -29,425 +29,367 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component" import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm); -export function DmsPostForm({bodyshop, socket, job, logsRef}) { - const [form] = Form.useForm(); - const {t} = useTranslation(); +export function DmsPostForm({ bodyshop, socket, job, logsRef }) { + const [form] = Form.useForm(); + const { t } = useTranslation(); - const handlePayerSelect = (value, index) => { - form.setFieldsValue({ - payers: form.getFieldValue("payers").map((payer, mapIndex) => { - if (index !== mapIndex) return payer; - const cdkPayer = - bodyshop.cdk_configuration.payers && - bodyshop.cdk_configuration.payers.find((i) => i.name === value); + const handlePayerSelect = (value, index) => { + form.setFieldsValue({ + payers: form.getFieldValue("payers").map((payer, mapIndex) => { + if (index !== mapIndex) return payer; + const cdkPayer = + bodyshop.cdk_configuration.payers && bodyshop.cdk_configuration.payers.find((i) => i.name === value); - if (!cdkPayer) return payer; + if (!cdkPayer) return payer; - return { - ...cdkPayer, - dms_acctnumber: cdkPayer.dms_acctnumber, - controlnumber: job && job[cdkPayer.control_type], - }; - }), + return { + ...cdkPayer, + dms_acctnumber: cdkPayer.dms_acctnumber, + controlnumber: job && job[cdkPayer.control_type] + }; + }) + }); + }; + + const handleFinish = (values) => { + socket.emit(`${determineDmsType(bodyshop)}-export-job`, { + jobid: job.id, + txEnvelope: values + }); + console.log(logsRef); + if (logsRef) { + console.log("executing", logsRef); + logsRef.curent && + logsRef.current.scrollIntoView({ + behavior: "smooth" }); - }; + } + }; - const handleFinish = (values) => { - socket.emit(`${determineDmsType(bodyshop)}-export-job`, { - jobid: job.id, - txEnvelope: values, - }); - console.log(logsRef); - if (logsRef) { - console.log("executing", logsRef); - logsRef.curent && - logsRef.current.scrollIntoView({ - behavior: "smooth", - }); - } - }; + return ( + + + + + + + + + + + + + - return ( - - - - + + + + + + + + + + + + + + + + + + + + + + + )} + + + + + + + + + + + + {(fields, { add, remove }) => { + return ( + + {fields.map((field, index) => ( + + + + handlePayerSelect(value, index)}> + {bodyshop.cdk_configuration && + bodyshop.cdk_configuration.payers && + bodyshop.cdk_configuration.payers.map((payer) => ( + {payer.name} + ))} + + + + + + + + + + + + + {t("jobs.fields.dms.payer.controlnumber")}{" "} + ({ + key: idx, + label: key.name, + onClick: () => { + form.setFieldsValue({ + payers: form.getFieldValue("payers").map((row, mapIndex) => { + if (index !== mapIndex) return row; + return { + ...row, + controlnumber: key.controlnumber + }; + }) + }); + } + })) + }} + > + e.preventDefault()}> + + + + } + key={`${index}controlnumber`} + name={[field.name, "controlnumber"]} rules={[ - { - required: true, - //message: t("general.validation.required"), - }, + { + required: true + } ]} - > - - - - - - - - - + > + + - {bodyshop.cdk_dealerid && ( - - - - - - - - - - - - - - - - - - - - - - - - )} - - - + + {() => { + const payers = form.getFieldValue("payers"); - - - - - - - - {(fields, {add, remove}) => { - return ( - - {fields.map((field, index) => ( - - - - handlePayerSelect(value, index)} - > - {bodyshop.cdk_configuration && - bodyshop.cdk_configuration.payers && - bodyshop.cdk_configuration.payers.map((payer) => ( - - {payer.name} - - ))} - - + const row = payers && payers[index]; - - - + const cdkPayer = + bodyshop.cdk_configuration.payers && + bodyshop.cdk_configuration.payers.find((i) => i && row && i.name === row.name); + if (i18n.exists(`jobs.fields.${cdkPayer?.control_type}`)) + return {cdkPayer && t(`jobs.fields.${cdkPayer?.control_type}`)}; + else if (i18n.exists(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)) { + return {cdkPayer && t(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)}; + } else { + return null; + } + }} + - - - - - - {t("jobs.fields.dms.payer.controlnumber")}{" "} - ({ - key: idx, - label: key.name, - onClick: () => { - form.setFieldsValue({ - payers: form.getFieldValue("payers").map((row, mapIndex) => { - if (index !== mapIndex) return row; - return { - ...row, - controlnumber: key.controlnumber, - }; - }), - }); - }, - })) - }}> - e.preventDefault()}> - - - - - } - key={`${index}controlnumber`} - name={[field.name, "controlnumber"]} - rules={[ - { - required: true, - }, - ]} - > - - - - - {() => { - const payers = form.getFieldValue("payers"); - - const row = payers && payers[index]; - - const cdkPayer = - bodyshop.cdk_configuration.payers && - bodyshop.cdk_configuration.payers.find( - (i) => i && row && i.name === row.name - ); - if ( - i18n.exists(`jobs.fields.${cdkPayer?.control_type}`) - ) - return ( - - {cdkPayer && - t(`jobs.fields.${cdkPayer?.control_type}`)} - - ); - else if ( - i18n.exists( - `jobs.fields.dms.control_type.${cdkPayer?.control_type}` - ) - ) { - return ( - - {cdkPayer && - t( - `jobs.fields.dms.control_type.${cdkPayer?.control_type}` - )} - - ); - } else { - return null; - } - }} - - - { - remove(field.name); - }} - /> - - - ))} - - { - if (fields.length < 3) add(); - }} - style={{width: "100%"}} - > - {t("jobs.actions.dms.addpayer")} - - - - ); - }} - - - {() => { - //Perform Calculation to determine discrepancy. - let totalAllocated = Dinero(); - - const payers = form.getFieldValue("payers"); - payers && - payers.forEach((payer) => { - totalAllocated = totalAllocated.add( - Dinero({amount: Math.round((payer?.amount || 0) * 100)}) - ); - }); - - const totals = - socket.allocationsSummary && - socket.allocationsSummary.reduce( - (acc, val) => { - return { - totalSale: acc.totalSale.add(Dinero(val.sale)), - totalCost: acc.totalCost.add(Dinero(val.cost)), - }; - }, - { - totalSale: Dinero(), - totalCost: Dinero(), - } - ); - const discrep = totals - ? totals.totalSale.subtract(totalAllocated) - : Dinero(); - return ( - - - - - - = - - - {t("jobs.actions.dms.post")} - - - ); + { + remove(field.name); + }} + /> + + + ))} + + { + if (fields.length < 3) add(); }} + style={{ width: "100%" }} + > + {t("jobs.actions.dms.addpayer")} + - - - ); + + ); + }} + + + {() => { + //Perform Calculation to determine discrepancy. + let totalAllocated = Dinero(); + + const payers = form.getFieldValue("payers"); + payers && + payers.forEach((payer) => { + totalAllocated = totalAllocated.add(Dinero({ amount: Math.round((payer?.amount || 0) * 100) })); + }); + + const totals = + socket.allocationsSummary && + socket.allocationsSummary.reduce( + (acc, val) => { + return { + totalSale: acc.totalSale.add(Dinero(val.sale)), + totalCost: acc.totalCost.add(Dinero(val.cost)) + }; + }, + { + totalSale: Dinero(), + totalCost: Dinero() + } + ); + const discrep = totals ? totals.totalSale.subtract(totalAllocated) : Dinero(); + return ( + + + - + + = + + + {t("jobs.actions.dms.post")} + + + ); + }} + + + + ); } diff --git a/client/src/components/document-editor/document-editor.component.jsx b/client/src/components/document-editor/document-editor.component.jsx index d1d1df491..70c7293f3 100644 --- a/client/src/components/document-editor/document-editor.component.jsx +++ b/client/src/components/document-editor/document-editor.component.jsx @@ -1,110 +1,101 @@ //import "tui-image-editor/dist/tui-image-editor.css"; -import {Result} from "antd"; +import { Result } from "antd"; import * as markerjs2 from "markerjs2"; -import React, {useCallback, useEffect, useRef, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {handleUpload} from "../documents-upload/documents-upload.utility"; -import {GenerateSrcUrl} from "../jobs-documents-gallery/job-documents.utility"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { handleUpload } from "../documents-upload/documents-upload.utility"; +import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function DocumentEditorComponent({currentUser, bodyshop, document}) { - const imgRef = useRef(null); - const [loading, setLoading] = useState(false); - const [uploaded, setuploaded] = useState(false); - const markerArea = useRef(null); - const {t} = useTranslation(); +export function DocumentEditorComponent({ currentUser, bodyshop, document }) { + const imgRef = useRef(null); + const [loading, setLoading] = useState(false); + const [uploaded, setuploaded] = useState(false); + const markerArea = useRef(null); + const { t } = useTranslation(); - const triggerUpload = useCallback( - async (dataUrl) => { - setLoading(true); - handleUpload( - { - filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`, - file: await b64toBlob(dataUrl), - onSuccess: () => { - setLoading(false); - setuploaded(true); - }, - onError: () => setLoading(false), - }, - { - bodyshop: bodyshop, - uploaded_by: currentUser.email, - jobId: document.jobid, - //billId: billId, - tagsArray: ["edited"], - //callback: callbackAfterUpload, - } - ); + const triggerUpload = useCallback( + async (dataUrl) => { + setLoading(true); + handleUpload( + { + filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`, + file: await b64toBlob(dataUrl), + onSuccess: () => { + setLoading(false); + setuploaded(true); + }, + onError: () => setLoading(false) }, - [bodyshop, currentUser, document] - ); - - useEffect(() => { - if (imgRef.current !== null) { - // create a marker.js MarkerArea - markerArea.current = new markerjs2.MarkerArea(imgRef.current); - - // attach an event handler to assign annotated image back to our image element - markerArea.current.addEventListener("close", (closeEvent) => { - }); - - markerArea.current.addEventListener("render", (event) => { - const dataUrl = event.dataUrl; - imgRef.current.src = dataUrl; - markerArea.current.close(); - triggerUpload(dataUrl); - }); - // launch marker.js - - markerArea.current.renderAtNaturalSize = true; - markerArea.current.renderImageType = "image/jpeg"; - markerArea.current.renderImageQuality = 1; - //markerArea.current.settings.displayMode = "inline"; - markerArea.current.show(); + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: document.jobid, + //billId: billId, + tagsArray: ["edited"] + //callback: callbackAfterUpload, } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [triggerUpload]); + ); + }, + [bodyshop, currentUser, document] + ); - async function b64toBlob(url) { - const res = await fetch(url); - return await res.blob(); + useEffect(() => { + if (imgRef.current !== null) { + // create a marker.js MarkerArea + markerArea.current = new markerjs2.MarkerArea(imgRef.current); + + // attach an event handler to assign annotated image back to our image element + markerArea.current.addEventListener("close", (closeEvent) => {}); + + markerArea.current.addEventListener("render", (event) => { + const dataUrl = event.dataUrl; + imgRef.current.src = dataUrl; + markerArea.current.close(); + triggerUpload(dataUrl); + }); + // launch marker.js + + markerArea.current.renderAtNaturalSize = true; + markerArea.current.renderImageType = "image/jpeg"; + markerArea.current.renderImageQuality = 1; + //markerArea.current.settings.displayMode = "inline"; + markerArea.current.show(); } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [triggerUpload]); - return ( - - {!loading && !uploaded && ( - - )} - {loading && } - {uploaded && ( - - )} - - ); + async function b64toBlob(url) { + const res = await fetch(url); + return await res.blob(); + } + + return ( + + {!loading && !uploaded && ( + + )} + {loading && } + {uploaded && } + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(DocumentEditorComponent); +export default connect(mapStateToProps, mapDispatchToProps)(DocumentEditorComponent); diff --git a/client/src/components/document-editor/document-editor.container.jsx b/client/src/components/document-editor/document-editor.container.jsx index fa79bc6c3..b5868e076 100644 --- a/client/src/components/document-editor/document-editor.container.jsx +++ b/client/src/components/document-editor/document-editor.container.jsx @@ -1,62 +1,55 @@ -import {useQuery} from "@apollo/client"; -import {Result} from "antd"; +import { useQuery } from "@apollo/client"; +import { Result } from "antd"; import queryString from "query-string"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useLocation} from "react-router-dom"; -import {QUERY_BODYSHOP} from "../../graphql/bodyshop.queries"; -import {GET_DOCUMENT_BY_PK} from "../../graphql/documents.queries"; -import {setBodyshop} from "../../redux/user/user.actions"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation } from "react-router-dom"; +import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; +import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries"; +import { setBodyshop } from "../../redux/user/user.actions"; import AlertComponent from "../alert/alert.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import DocumentEditor from "./document-editor.component"; const mapDispatchToProps = (dispatch) => ({ - setBodyshop: (bs) => dispatch(setBodyshop(bs)), + setBodyshop: (bs) => dispatch(setBodyshop(bs)) }); export default connect(null, mapDispatchToProps)(DocumentEditorContainer); -export function DocumentEditorContainer({setBodyshop}) { - //Get the image details for the image to be saved. - //Get the document id from the search string. - const {documentId} = queryString.parse(useLocation().search); - const {t} = useTranslation(); - const { - loading: loadingShop, - error: errorShop, - data: dataShop, - } = useQuery(QUERY_BODYSHOP, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +export function DocumentEditorContainer({ setBodyshop }) { + //Get the image details for the image to be saved. + //Get the document id from the search string. + const { documentId } = queryString.parse(useLocation().search); + const { t } = useTranslation(); + const { + loading: loadingShop, + error: errorShop, + data: dataShop + } = useQuery(QUERY_BODYSHOP, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - useEffect(() => { - if (dataShop) setBodyshop(dataShop.bodyshops[0]); - }, [dataShop, setBodyshop]); + useEffect(() => { + if (dataShop) setBodyshop(dataShop.bodyshops[0]); + }, [dataShop, setBodyshop]); - const {loading, error, data} = useQuery(GET_DOCUMENT_BY_PK, { - variables: {documentId}, - skip: !documentId, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, { + variables: { documentId }, + skip: !documentId, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - if (loading || loadingShop) return ; - if (error || errorShop) - return ( - - ); + if (loading || loadingShop) return ; + if (error || errorShop) return ; - if (!data || !data.documents_by_pk) - return ; - return ( - - - - ); + if (!data || !data.documents_by_pk) return ; + return ( + + + + ); } diff --git a/client/src/components/documents-local-upload/documents-local-upload.component.jsx b/client/src/components/documents-local-upload/documents-local-upload.component.jsx index 087c6f255..d305b1677 100644 --- a/client/src/components/documents-local-upload/documents-local-upload.component.jsx +++ b/client/src/components/documents-local-upload/documents-local-upload.component.jsx @@ -1,71 +1,69 @@ -import {UploadOutlined} from "@ant-design/icons"; -import {Upload} from "antd"; -import React, {useState} from "react"; +import { UploadOutlined } from "@ant-design/icons"; +import { Upload } from "antd"; +import React, { useState } from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {handleUpload} from "./documents-local-upload.utility"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { handleUpload } from "./documents-local-upload.utility"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); export function DocumentsLocalUploadComponent({ - children, - currentUser, - bodyshop, - job, - vendorid, - invoice_number, - callbackAfterUpload, - allowAllTypes, - }) { - const [fileList, setFileList] = useState([]); + children, + currentUser, + bodyshop, + job, + vendorid, + invoice_number, + callbackAfterUpload, + allowAllTypes +}) { + const [fileList, setFileList] = useState([]); - const handleDone = (uid) => { - setTimeout(() => { - setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); - }, 2000); - }; + const handleDone = (uid) => { + setTimeout(() => { + setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); + }, 2000); + }; - return ( - { - if (f.event && f.event.percent === 100) handleDone(f.file.uid); + return ( + { + if (f.event && f.event.percent === 100) handleDone(f.file.uid); - setFileList(f.fileList); - }} - customRequest={(ev) => - handleUpload({ - ev, - context: { - jobid: job.id, - vendorid, - invoice_number, - callback: callbackAfterUpload, - }, - }) - } - {...(!allowAllTypes && { - accept: "audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx", - })} - > - {children || ( - <> - - - - - Click or drag files to this area to upload. - - > - )} - - ); + setFileList(f.fileList); + }} + customRequest={(ev) => + handleUpload({ + ev, + context: { + jobid: job.id, + vendorid, + invoice_number, + callback: callbackAfterUpload + } + }) + } + {...(!allowAllTypes && { + accept: "audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx" + })} + > + {children || ( + <> + + + + Click or drag files to this area to upload. + > + )} + + ); } export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent); diff --git a/client/src/components/documents-local-upload/documents-local-upload.utility.js b/client/src/components/documents-local-upload/documents-local-upload.utility.js index eb9a97ed7..833ceedcf 100644 --- a/client/src/components/documents-local-upload/documents-local-upload.utility.js +++ b/client/src/components/documents-local-upload/documents-local-upload.utility.js @@ -1,80 +1,72 @@ import cleanAxios from "../../utils/CleanAxios"; -import {store} from "../../redux/store"; -import {addMediaForJob} from "../../redux/media/media.actions"; +import { store } from "../../redux/store"; +import { addMediaForJob } from "../../redux/media/media.actions"; import normalizeUrl from "normalize-url"; -import {notification} from "antd"; +import { notification } from "antd"; import i18n from "i18next"; -export const handleUpload = async ({ev, context}) => { - const {onError, onSuccess, onProgress, file} = ev; - const {jobid, invoice_number, vendorid, callbackAfterUpload} = context; +export const handleUpload = async ({ ev, context }) => { + const { onError, onSuccess, onProgress, file } = ev; + const { jobid, invoice_number, vendorid, callbackAfterUpload } = context; - const bodyshop = store.getState().user.bodyshop; - var options = { - headers: { - "X-Requested-With": "XMLHttpRequest", - ims_token: bodyshop.localmediatoken, - }, - onUploadProgress: (e) => { - if (!!onProgress) onProgress({percent: (e.loaded / e.total) * 100}); - }, - }; - - const formData = new FormData(); - - formData.append("jobid", jobid); - if (invoice_number) { - formData.append("invoice_number", invoice_number); - formData.append("vendorid", vendorid); + const bodyshop = store.getState().user.bodyshop; + var options = { + headers: { + "X-Requested-With": "XMLHttpRequest", + ims_token: bodyshop.localmediatoken + }, + onUploadProgress: (e) => { + if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); } - formData.append("file", file); + }; - const imexMediaServerResponse = await cleanAxios.post( - normalizeUrl( - `${bodyshop.localmediaserverhttp}/${ - invoice_number ? "bills" : "jobs" - }/upload` - ), - formData, - { - ...options, - } + const formData = new FormData(); + + formData.append("jobid", jobid); + if (invoice_number) { + formData.append("invoice_number", invoice_number); + formData.append("vendorid", vendorid); + } + formData.append("file", file); + + const imexMediaServerResponse = await cleanAxios.post( + normalizeUrl(`${bodyshop.localmediaserverhttp}/${invoice_number ? "bills" : "jobs"}/upload`), + formData, + { + ...options + } + ); + + if (imexMediaServerResponse.status !== 200) { + if (!!onError) { + onError(imexMediaServerResponse.statusText); + } + } else { + onSuccess && onSuccess(file); + notification.open({ + type: "success", + key: "docuploadsuccess", + message: i18n.t("documents.successes.insert") + }); + store.dispatch( + addMediaForJob({ + jobid, + media: imexMediaServerResponse.data.map((d) => { + return { + ...d, + selected: false, + src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`), + ...(d.optimized && { + optimized: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.optimized}`) + }), + thumbnail: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.thumbnail}`) + }; + }) + }) ); + } - if (imexMediaServerResponse.status !== 200) { - if (!!onError) { - onError(imexMediaServerResponse.statusText); - } - } else { - onSuccess && onSuccess(file); - notification.open({ - type: "success", - key: "docuploadsuccess", - message: i18n.t("documents.successes.insert"), - }); - store.dispatch( - addMediaForJob({ - jobid, - media: imexMediaServerResponse.data.map((d) => { - return { - ...d, - selected: false, - src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`), - ...(d.optimized && { - optimized: normalizeUrl( - `${bodyshop.localmediaserverhttp}/${d.optimized}` - ), - }), - thumbnail: normalizeUrl( - `${bodyshop.localmediaserverhttp}/${d.thumbnail}` - ), - }; - }), - }) - ); - } - - if (callbackAfterUpload) { - callbackAfterUpload(); - } + if (callbackAfterUpload) { + callbackAfterUpload(); + } }; diff --git a/client/src/components/documents-upload/documents-upload.component.jsx b/client/src/components/documents-upload/documents-upload.component.jsx index 37537f251..6392575ec 100644 --- a/client/src/components/documents-upload/documents-upload.component.jsx +++ b/client/src/components/documents-upload/documents-upload.component.jsx @@ -1,117 +1,111 @@ -import {UploadOutlined} from "@ant-design/icons"; -import {notification, Progress, Result, Space, Upload} from "antd"; -import React, {useMemo, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import { UploadOutlined } from "@ant-design/icons"; +import { notification, Progress, Result, Space, Upload } from "antd"; +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import formatBytes from "../../utils/formatbytes"; -import {handleUpload} from "./documents-upload.utility"; +import { handleUpload } from "./documents-upload.utility"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); export function DocumentsUploadComponent({ - children, - currentUser, - bodyshop, - jobId, - tagsArray, - billId, - callbackAfterUpload, - totalSize, - ignoreSizeLimit = false, - }) { - const {t} = useTranslation(); - const [fileList, setFileList] = useState([]); + children, + currentUser, + bodyshop, + jobId, + tagsArray, + billId, + callbackAfterUpload, + totalSize, + ignoreSizeLimit = false +}) { + const { t } = useTranslation(); + const [fileList, setFileList] = useState([]); - const pct = useMemo(() => { - return parseInt( - (totalSize / ((bodyshop && bodyshop.jobsizelimit) || 1)) * 100 - ); - }, [bodyshop, totalSize]); - - if (pct > 100 && !ignoreSizeLimit) - return ( - - ); - - const handleDone = (uid) => { - setTimeout(() => { - setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); - }, 2000); - }; + const pct = useMemo(() => { + return parseInt((totalSize / ((bodyshop && bodyshop.jobsizelimit) || 1)) * 100); + }, [bodyshop, totalSize]); + if (pct > 100 && !ignoreSizeLimit) return ( - { - if (f.event && f.event.percent === 100) handleDone(f.file.uid); - setFileList(f.fileList); - }} - beforeUpload={(file, fileList) => { - if (ignoreSizeLimit) return true; - const newFiles = fileList.reduce((acc, val) => acc + val.size, 0); - const shouldStopUpload = - (totalSize + newFiles) / ((bodyshop && bodyshop.jobsizelimit) || 1) >= - 1; + + ); - //Check to see if old files plus newly uploaded ones will be too much. - if (shouldStopUpload) { - notification.open({ - key: "cannotuploaddocuments", - type: "error", - message: t("documents.labels.upload_limitexceeded_title"), - description: t("documents.labels.upload_limitexceeded"), - }); - return Upload.LIST_IGNORE; - } - return true; - }} - customRequest={(ev) => - handleUpload(ev, { - bodyshop: bodyshop, - uploaded_by: currentUser.email, - jobId: jobId, - billId: billId, - tagsArray: tagsArray, - callback: callbackAfterUpload, - }) - } - accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx" - // showUploadList={false} - > - {children || ( - <> - - - - - Click or drag files to this area to upload. - - {!ignoreSizeLimit && ( - - - + const handleDone = (uid) => { + setTimeout(() => { + setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); + }, 2000); + }; + + return ( + { + if (f.event && f.event.percent === 100) handleDone(f.file.uid); + setFileList(f.fileList); + }} + beforeUpload={(file, fileList) => { + if (ignoreSizeLimit) return true; + const newFiles = fileList.reduce((acc, val) => acc + val.size, 0); + const shouldStopUpload = (totalSize + newFiles) / ((bodyshop && bodyshop.jobsizelimit) || 1) >= 1; + + //Check to see if old files plus newly uploaded ones will be too much. + if (shouldStopUpload) { + notification.open({ + key: "cannotuploaddocuments", + type: "error", + message: t("documents.labels.upload_limitexceeded_title"), + description: t("documents.labels.upload_limitexceeded") + }); + return Upload.LIST_IGNORE; + } + return true; + }} + customRequest={(ev) => + handleUpload(ev, { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: jobId, + billId: billId, + tagsArray: tagsArray, + callback: callbackAfterUpload + }) + } + accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx" + // showUploadList={false} + > + {children || ( + <> + + + + Click or drag files to this area to upload. + {!ignoreSizeLimit && ( + + + {t("documents.labels.usage", { - percent: pct, - used: formatBytes(totalSize), - total: formatBytes(bodyshop && bodyshop.jobsizelimit), + percent: pct, + used: formatBytes(totalSize), + total: formatBytes(bodyshop && bodyshop.jobsizelimit) })} - - )} - > - )} - - ); + + )} + > + )} + + ); } export default connect(mapStateToProps, null)(DocumentsUploadComponent); diff --git a/client/src/components/documents-upload/documents-upload.utility.js b/client/src/components/documents-upload/documents-upload.utility.js index 7d62e6e85..6e2e12c24 100644 --- a/client/src/components/documents-upload/documents-upload.utility.js +++ b/client/src/components/documents-upload/documents-upload.utility.js @@ -1,12 +1,12 @@ -import {notification} from "antd"; +import { notification } from "antd"; import axios from "axios"; import i18n from "i18next"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {INSERT_NEW_DOCUMENT} from "../../graphql/documents.queries"; -import {axiosAuthInterceptorId} from "../../utils/CleanAxios"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries"; +import { axiosAuthInterceptorId } from "../../utils/CleanAxios"; import client from "../../utils/GraphQLClient"; import exifr from "exifr"; -import {store} from "../../redux/store"; +import { store } from "../../redux/store"; //Context: currentUserEmail, bodyshop, jobid, invoiceid @@ -15,86 +15,61 @@ var cleanAxios = axios.create(); cleanAxios.interceptors.request.eject(axiosAuthInterceptorId); export const handleUpload = (ev, context) => { - logImEXEvent("document_upload", {filetype: ev.file.type}); + logImEXEvent("document_upload", { filetype: ev.file.type }); - const {onError, onSuccess, onProgress} = ev; - const {bodyshop, jobId} = context; + const { onError, onSuccess, onProgress } = ev; + const { bodyshop, jobId } = context; - const fileName = ev.file.name || ev.filename; + const fileName = ev.file.name || ev.filename; - let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace( - /[^A-Z0-9]+/gi, - "_" - )}-${new Date().getTime()}`; - let extension = fileName.split(".").pop(); - uploadToCloudinary( - key, - extension, - ev.file.type, - ev.file, - onError, - onSuccess, - onProgress, - context - ); + let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}`; + let extension = fileName.split(".").pop(); + uploadToCloudinary(key, extension, ev.file.type, ev.file, onError, onSuccess, onProgress, context); }; -export const uploadToCloudinary = async ( - key, - extension, - fileType, - file, - onError, - onSuccess, - onProgress, - context -) => { - const {bodyshop, jobId, billId, uploaded_by, callback, tagsArray} = context; +export const uploadToCloudinary = async (key, extension, fileType, file, onError, onSuccess, onProgress, context) => { + const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray } = context; - //Set variables for getting the signed URL. - let timestamp = Math.floor(Date.now() / 1000); - let public_id = key; - let tags = `${bodyshop.imexshopid},${ - tagsArray ? tagsArray.map((tag) => `${tag},`) : "" - }`; - // let eager = import.meta.env.VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS; + //Set variables for getting the signed URL. + let timestamp = Math.floor(Date.now() / 1000); + let public_id = key; + let tags = `${bodyshop.imexshopid},${tagsArray ? tagsArray.map((tag) => `${tag},`) : ""}`; + // let eager = import.meta.env.VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS; - //Get the signed url. + //Get the signed url. - const upload_preset = fileType.startsWith("video") - ? "incoming_upload_video" - : "incoming_upload"; + const upload_preset = fileType.startsWith("video") ? "incoming_upload_video" : "incoming_upload"; - const signedURLResponse = await axios.post("/media/sign", { - public_id: public_id, - tags: tags, - timestamp: timestamp, - upload_preset: upload_preset, + const signedURLResponse = await axios.post("/media/sign", { + public_id: public_id, + tags: tags, + timestamp: timestamp, + upload_preset: upload_preset + }); + + if (signedURLResponse.status !== 200) { + if (!!onError) onError(signedURLResponse.statusText); + notification["error"]({ + message: i18n.t("documents.errors.getpresignurl", { + message: signedURLResponse.statusText + }) }); + return; + } - if (signedURLResponse.status !== 200) { - if (!!onError) onError(signedURLResponse.statusText); - notification["error"]({ - message: i18n.t("documents.errors.getpresignurl", { - message: signedURLResponse.statusText, - }), - }); - return; + //Build request to end to cloudinary. + var signature = signedURLResponse.data; + var options = { + headers: { "X-Requested-With": "XMLHttpRequest" }, + onUploadProgress: (e) => { + if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); } + }; - //Build request to end to cloudinary. - var signature = signedURLResponse.data; - var options = { - headers: {"X-Requested-With": "XMLHttpRequest"}, - onUploadProgress: (e) => { - if (!!onProgress) onProgress({percent: (e.loaded / e.total) * 100}); - }, - }; + const formData = new FormData(); + formData.append("file", file); - const formData = new FormData(); - formData.append("file", file); - - formData.append("upload_preset", upload_preset); + formData.append("upload_preset", upload_preset); formData.append("api_key", import.meta.env.VITE_APP_CLOUDINARY_API_KEY); formData.append("public_id", public_id); @@ -103,131 +78,128 @@ export const uploadToCloudinary = async ( formData.append("signature", signature); const cloudinaryUploadResponse = await cleanAxios.post( - `${import.meta.env.VITE_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType( - fileType - )}/upload`, + `${import.meta.env.VITE_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(fileType)}/upload`, formData, { - ...options, + ...options } ); - if (cloudinaryUploadResponse.status !== 200) { - if (!!onError) { - onError(cloudinaryUploadResponse.statusText); - } - - try { - axios.post("/newlog", { - message: "client-cloudinary-upload-error", - type: "error", - user: store.getState().user.email, - object: cloudinaryUploadResponse, - }); - } catch (error) { - } - - notification["error"]({ - message: i18n.t("documents.errors.insert", { - message: cloudinaryUploadResponse.statusText, - }), - }); - return; + if (cloudinaryUploadResponse.status !== 200) { + if (!!onError) { + onError(cloudinaryUploadResponse.statusText); } - //Insert the document with the matching key. - let takenat; - if (fileType.includes("image")) { - try { - const exif = await exifr.parse(file); + try { + axios.post("/newlog", { + message: "client-cloudinary-upload-error", + type: "error", + user: store.getState().user.email, + object: cloudinaryUploadResponse + }); + } catch (error) {} - takenat = exif && exif.DateTimeOriginal; - } catch (error) { - console.log("Unable to parse image file for EXIF Data"); - } - } - const documentInsert = await client.mutate({ - mutation: INSERT_NEW_DOCUMENT, - variables: { - docInput: [ - { - ...(jobId ? {jobid: jobId} : {}), - ...(billId ? {billid: billId} : {}), - uploaded_by: uploaded_by, - key: key, - type: fileType, - extension: cloudinaryUploadResponse.data.format || extension, - bodyshopid: bodyshop.id, - size: cloudinaryUploadResponse.data.bytes || file.size, - takenat, - }, - ], - }, + notification["error"]({ + message: i18n.t("documents.errors.insert", { + message: cloudinaryUploadResponse.statusText + }) }); - if (!documentInsert.errors) { - if (!!onSuccess) - onSuccess({ - uid: documentInsert.data.insert_documents.returning[0].id, - name: documentInsert.data.insert_documents.returning[0].name, - status: "done", - key: documentInsert.data.insert_documents.returning[0].key, - }); - notification.open({ - type: "success", - key: "docuploadsuccess", - message: i18n.t("documents.successes.insert"), - }); - if (callback) { - callback(); - } - } else { - if (!!onError) onError(JSON.stringify(documentInsert.errors)); - notification["error"]({ - message: i18n.t("documents.errors.insert", { - message: JSON.stringify(documentInsert.errors), - }), - }); - return; + return; + } + + //Insert the document with the matching key. + let takenat; + if (fileType.includes("image")) { + try { + const exif = await exifr.parse(file); + + takenat = exif && exif.DateTimeOriginal; + } catch (error) { + console.log("Unable to parse image file for EXIF Data"); } + } + const documentInsert = await client.mutate({ + mutation: INSERT_NEW_DOCUMENT, + variables: { + docInput: [ + { + ...(jobId ? { jobid: jobId } : {}), + ...(billId ? { billid: billId } : {}), + uploaded_by: uploaded_by, + key: key, + type: fileType, + extension: cloudinaryUploadResponse.data.format || extension, + bodyshopid: bodyshop.id, + size: cloudinaryUploadResponse.data.bytes || file.size, + takenat + } + ] + } + }); + if (!documentInsert.errors) { + if (!!onSuccess) + onSuccess({ + uid: documentInsert.data.insert_documents.returning[0].id, + name: documentInsert.data.insert_documents.returning[0].name, + status: "done", + key: documentInsert.data.insert_documents.returning[0].key + }); + notification.open({ + type: "success", + key: "docuploadsuccess", + message: i18n.t("documents.successes.insert") + }); + if (callback) { + callback(); + } + } else { + if (!!onError) onError(JSON.stringify(documentInsert.errors)); + notification["error"]({ + message: i18n.t("documents.errors.insert", { + message: JSON.stringify(documentInsert.errors) + }) + }); + return; + } }; //Also needs to be updated in media JS and mobile app. export function DetermineFileType(filetype) { - if (!filetype) return "auto"; - else if (filetype.startsWith("image")) return "image"; - else if (filetype.startsWith("video")) return "video"; - else if (filetype.startsWith("application/pdf")) return "image"; - else if (filetype.startsWith("application")) return "raw"; + if (!filetype) return "auto"; + else if (filetype.startsWith("image")) return "image"; + else if (filetype.startsWith("video")) return "video"; + else if (filetype.startsWith("application/pdf")) return "image"; + else if (filetype.startsWith("application")) return "raw"; - return "auto"; + return "auto"; } function replaceAccents(str) { - // Verifies if the String has accents and replace them - if (str.search(/[\xC0-\xFF]/g) > -1) { - str = str - .replace(/[\xC0-\xC5]/g, "A") - .replace(/[\xC6]/g, "AE") - .replace(/[\xC7]/g, "C") - .replace(/[\xC8-\xCB]/g, "E") - .replace(/[\xCC-\xCF]/g, "I") - .replace(/[\xD0]/g, "D") - .replace(/[\xD1]/g, "N") - .replace(/[\xD2-\xD6\xD8]/g, "O") - .replace(/[\xD9-\xDC]/g, "U") - .replace(/[\xDD]/g, "Y") - .replace(/[\xDE]/g, "P") - .replace(/[\xE0-\xE5]/g, "a") - .replace(/[\xE6]/g, "ae") - .replace(/[\xE7]/g, "c") - .replace(/[\xE8-\xEB]/g, "e") - .replace(/[\xEC-\xEF]/g, "i") - .replace(/[\xF1]/g, "n") - .replace(/[\xF2-\xF6\xF8]/g, "o") - .replace(/[\xF9-\xFC]/g, "u") - .replace(/[\xFE]/g, "p") - .replace(/[\xFD\xFF]/g, "y"); - } + // Verifies if the String has accents and replace them + if (str.search(/[\xC0-\xFF]/g) > -1) { + str = str + .replace(/[\xC0-\xC5]/g, "A") + .replace(/[\xC6]/g, "AE") + .replace(/[\xC7]/g, "C") + .replace(/[\xC8-\xCB]/g, "E") + .replace(/[\xCC-\xCF]/g, "I") + .replace(/[\xD0]/g, "D") + .replace(/[\xD1]/g, "N") + .replace(/[\xD2-\xD6\xD8]/g, "O") + .replace(/[\xD9-\xDC]/g, "U") + .replace(/[\xDD]/g, "Y") + .replace(/[\xDE]/g, "P") + .replace(/[\xE0-\xE5]/g, "a") + .replace(/[\xE6]/g, "ae") + .replace(/[\xE7]/g, "c") + .replace(/[\xE8-\xEB]/g, "e") + .replace(/[\xEC-\xEF]/g, "i") + .replace(/[\xF1]/g, "n") + .replace(/[\xF2-\xF6\xF8]/g, "o") + .replace(/[\xF9-\xFC]/g, "u") + .replace(/[\xFE]/g, "p") + .replace(/[\xFD\xFF]/g, "y"); + } - return str; + return str; } diff --git a/client/src/components/email-documents/email-documents.component.jsx b/client/src/components/email-documents/email-documents.component.jsx index 5ca6eae75..d3c6b20da 100644 --- a/client/src/components/email-documents/email-documents.component.jsx +++ b/client/src/components/email-documents/email-documents.component.jsx @@ -1,74 +1,63 @@ -import {useQuery} from "@apollo/client"; +import { useQuery } from "@apollo/client"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries"; -import {selectEmailConfig} from "../../redux/email/email.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries"; +import { selectEmailConfig } from "../../redux/email/email.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; -import JobsDocumentsLocalGalleryExternalComponent - from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; +import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, - emailConfig: selectEmailConfig, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + emailConfig: selectEmailConfig }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(EmailDocumentsComponent); +export default connect(mapStateToProps, mapDispatchToProps)(EmailDocumentsComponent); -export function EmailDocumentsComponent({ - emailConfig, - form, - selectedMediaState, - bodyshop, - }) { - const {t} = useTranslation(); +export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState, bodyshop }) { + const { t } = useTranslation(); - const [selectedMedia, setSelectedMedia] = selectedMediaState; - const {loading, error, data} = useQuery(GET_DOCUMENTS_BY_JOB, { - variables: { - jobId: emailConfig.jobid, - }, - skip: !emailConfig.jobid, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const [selectedMedia, setSelectedMedia] = selectedMediaState; + const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { + variables: { + jobId: emailConfig.jobid + }, + skip: !emailConfig.jobid, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - return ( - - {loading && } - {error && } - {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( - {t("messaging.labels.maxtenimages")} - ) : null} - {selectedMedia && - selectedMedia - .filter((s) => s.isSelected) - .reduce((acc, val) => (acc = acc + val.size), 0) >= - 10485760 - new Blob([form.getFieldValue("html")]).size ? ( - {t("general.errors.sizelimit")} - ) : null} - {!bodyshop.uselocalmediaserver && data && ( - - )} - {bodyshop.uselocalmediaserver && ( - - )} - - ); + return ( + + {loading && } + {error && } + {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( + {t("messaging.labels.maxtenimages")} + ) : null} + {selectedMedia && + selectedMedia.filter((s) => s.isSelected).reduce((acc, val) => (acc = acc + val.size), 0) >= + 10485760 - new Blob([form.getFieldValue("html")]).size ? ( + {t("general.errors.sizelimit")} + ) : null} + {!bodyshop.uselocalmediaserver && data && ( + + )} + {bodyshop.uselocalmediaserver && ( + + )} + + ); } diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index d648eddb0..fe14137c1 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -1,262 +1,217 @@ -import {UploadOutlined, UserAddOutlined} from "@ant-design/icons"; -import {Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload,} from "antd"; +import { UploadOutlined, UserAddOutlined } from "@ant-design/icons"; +import { Button, Divider, Dropdown, Form, Input, Select, Space, Tabs, Upload } from "antd"; import _ from "lodash"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectEmailConfig} from "../../redux/email/email.selectors"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {CreateExplorerLinkForJob} from "../../utils/localmedia"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectEmailConfig } from "../../redux/email/email.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { CreateExplorerLinkForJob } from "../../utils/localmedia"; import EmailDocumentsComponent from "../email-documents/email-documents.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, - emailConfig: selectEmailConfig, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + emailConfig: selectEmailConfig }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(EmailOverlayComponent); +export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayComponent); -export function EmailOverlayComponent({ - emailConfig, - form, - selectedMediaState, - bodyshop, - currentUser, - }) { - const {t} = useTranslation(); - const handleClick = ({item, key, keyPath}) => { - const email = item.props.value; - form.setFieldsValue({ - to: _.uniq([ - ...form.getFieldValue("to"), - ...(typeof email === "string" ? [email] : email), - ]), - }); - }; - const handle_CC_Click = ({item, key, keyPath}) => { - const email = item.props.value; - form.setFieldsValue({ - cc: _.uniq([ - ...(form.getFieldValue("cc") || ""), - ...(typeof email === "string" ? [email] : email), - ]), - }); - }; +export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, bodyshop, currentUser }) { + const { t } = useTranslation(); + const handleClick = ({ item, key, keyPath }) => { + const email = item.props.value; + form.setFieldsValue({ + to: _.uniq([...form.getFieldValue("to"), ...(typeof email === "string" ? [email] : email)]) + }); + }; + const handle_CC_Click = ({ item, key, keyPath }) => { + const email = item.props.value; + form.setFieldsValue({ + cc: _.uniq([...(form.getFieldValue("cc") || ""), ...(typeof email === "string" ? [email] : email)]) + }); + }; - const emailsToMenu = { - items: [ - ...bodyshop.employees - .filter((e) => e.user_email) - .map((e, idx) => ({ - key: idx, - label: `${e.first_name} ${e.last_name}`, - value: e.user_email, - })), - ...bodyshop.md_to_emails.map((e, idx) => ({ - key: idx + "group", - label: e.label, - value: e.emails, - })), - ], - onClick: handleClick, - }; - const menuCC = { - items: [ - ...bodyshop.employees - .filter((e) => e.user_email) - .map((e, idx) => ({ - key: idx, - label: `${e.first_name} ${e.last_name}`, - value: e.user_email, - })), - ...bodyshop.md_to_emails.map((e, idx) => ({ - key: idx + "group", - label: e.label, - value: e.emails, - })), - ], - onClick: handle_CC_Click, - }; + const emailsToMenu = { + items: [ + ...bodyshop.employees + .filter((e) => e.user_email) + .map((e, idx) => ({ + key: idx, + label: `${e.first_name} ${e.last_name}`, + value: e.user_email + })), + ...bodyshop.md_to_emails.map((e, idx) => ({ + key: idx + "group", + label: e.label, + value: e.emails + })) + ], + onClick: handleClick + }; + const menuCC = { + items: [ + ...bodyshop.employees + .filter((e) => e.user_email) + .map((e, idx) => ({ + key: idx, + label: `${e.first_name} ${e.last_name}`, + value: e.user_email + })), + ...bodyshop.md_to_emails.map((e, idx) => ({ + key: idx + "group", + label: e.label, + value: e.emails + })) + ], + onClick: handle_CC_Click + }; - return ( - - - - - {currentUser.email} - - {bodyshop.email} - {bodyshop.md_from_emails && - bodyshop.md_from_emails.map((e) => ( - {e} - ))} - - - - {t("emails.fields.to")} - - e.preventDefault()} - > - - - - - } - name="to" - rules={[ - { - required: true, - //message: t("general.validation.required"), - }, - ]} - > - - - - {t("emails.fields.cc")} - - e.preventDefault()} - > - - - - - } - name="cc" - > - - - - - + return ( + + + + {currentUser.email} + {bodyshop.email} + {bodyshop.md_from_emails && bodyshop.md_from_emails.map((e) => {e})} + + + + {t("emails.fields.to")} + + e.preventDefault()}> + + + + + } + name="to" + rules={[ + { + required: true + //message: t("general.validation.required"), + } + ]} + > + + + + {t("emails.fields.cc")} + + e.preventDefault()}> + + + + + } + name="cc" + > + + + + + - {t("emails.labels.preview")} - {bodyshop.attach_pdf_to_email && ( - {t("emails.labels.pdfcopywillbeattached")} - )} + {t("emails.labels.preview")} + {bodyshop.attach_pdf_to_email && {t("emails.labels.pdfcopywillbeattached")}} - - {() => { - return ( - + {() => { + return ( + - ); - }} - - - - ), - }, - { - key: "attachments", - label: t("emails.labels.attachments"), - children: ( - <> - {bodyshop.uselocalmediaserver && emailConfig.jobid && ( - - {t("documents.labels.openinexplorer")} - - )} - { - if (Array.isArray(e)) { - return e; - } - return e && e.fileList; - }} - rules={[ - ({getFieldValue}) => ({ - validator(rule, value) { - const totalSize = value.reduce( - (acc, val) => (acc = acc + val.size), - 0 - ); - - const limit = - 10485760 - new Blob([form.getFieldValue("html")]).size; - - if (totalSize > limit) { - return Promise.reject(t("general.errors.sizelimit")); - } - return Promise.resolve(); - }, - }), - ]} - > - - <> - - - - - Click or drag files to this area to upload. - - > - - - > - ), - }, - ]} + backgroundColor: "lightgray", + borderLeft: "6px solid #2196F3" + }} + dangerouslySetInnerHTML={{ __html: form.getFieldValue("html") }} /> - - ); + ); + }} + + + + }, + { + key: "attachments", + label: t("emails.labels.attachments"), + children: ( + <> + {bodyshop.uselocalmediaserver && emailConfig.jobid && ( + + {t("documents.labels.openinexplorer")} + + )} + { + if (Array.isArray(e)) { + return e; + } + return e && e.fileList; + }} + rules={[ + ({ getFieldValue }) => ({ + validator(rule, value) { + const totalSize = value.reduce((acc, val) => (acc = acc + val.size), 0); + + const limit = 10485760 - new Blob([form.getFieldValue("html")]).size; + + if (totalSize > limit) { + return Promise.reject(t("general.errors.sizelimit")); + } + return Promise.resolve(); + } + }) + ]} + > + + <> + + + + Click or drag files to this area to upload. + > + + + > + ) + } + ]} + /> + + ); } diff --git a/client/src/components/email-overlay/email-overlay.container.jsx b/client/src/components/email-overlay/email-overlay.container.jsx index da51ea1c4..73d1d52c4 100644 --- a/client/src/components/email-overlay/email-overlay.container.jsx +++ b/client/src/components/email-overlay/email-overlay.container.jsx @@ -1,260 +1,224 @@ -import {Button, Divider, Form, Modal, notification, Space} from "antd"; +import { Button, Divider, Form, Modal, notification, Space } from "antd"; import axios from "axios"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {toggleEmailOverlayVisible} from "../../redux/email/email.actions"; -import {selectEmailConfig, selectEmailVisible,} from "../../redux/email/email.selectors.js"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { toggleEmailOverlayVisible } from "../../redux/email/email.actions"; +import { selectEmailConfig, selectEmailVisible } from "../../redux/email/email.selectors.js"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import RenderTemplate from "../../utils/RenderTemplate"; -import {EmailSettings} from "../../utils/TemplateConstants"; +import { EmailSettings } from "../../utils/TemplateConstants"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import EmailOverlayComponent from "./email-overlay.component"; const mapStateToProps = createStructuredSelector({ - modalVisible: selectEmailVisible, - emailConfig: selectEmailConfig, - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, + modalVisible: selectEmailVisible, + emailConfig: selectEmailConfig, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible()), + toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible()) }); -export function EmailOverlayContainer({ - emailConfig, - modalVisible, - toggleEmailOverlayVisible, - bodyshop, - currentUser, - }) { - const {t} = useTranslation(); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [sending, setSending] = useState(false); - const [rawHtml, setRawHtml] = useState(""); - const [pdfCopytoAttach, setPdfCopytoAttach] = useState({ - filename: null, - pdf: null, - }); - const [selectedMedia, setSelectedMedia] = useState([]); +export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOverlayVisible, bodyshop, currentUser }) { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [sending, setSending] = useState(false); + const [rawHtml, setRawHtml] = useState(""); + const [pdfCopytoAttach, setPdfCopytoAttach] = useState({ + filename: null, + pdf: null + }); + const [selectedMedia, setSelectedMedia] = useState([]); - const defaultEmailFrom = { - from: { - name: currentUser.displayName - ? `${currentUser.displayName} @ ${bodyshop.shopname}` - : bodyshop.shopname, - address: EmailSettings.fromAddress, + const defaultEmailFrom = { + from: { + name: currentUser.displayName ? `${currentUser.displayName} @ ${bodyshop.shopname}` : bodyshop.shopname, + address: EmailSettings.fromAddress + } + }; + + const handleFinish = async (allValues) => { + logImEXEvent("email_send_from_modal"); + + //const attachments = []; + + // if (values.fileList) + // await asyncForEach(values.fileList, async (f) => { + // const t = { + // ContentType: f.type, + // Filename: f.name, + // Base64Content: (await toBase64(f.originFileObj)).split(",")[1], + // }; + // attachments.push(t); + // }); + + const { from, ...values } = allValues; + setSending(true); + try { + await axios.post("/sendemail", { + bodyshopid: bodyshop.id, + jobid: emailConfig.jobid, + + ...defaultEmailFrom, + ReplyTo: { + Email: from, + Name: currentUser.displayName }, - }; + ...values, + html: rawHtml, + attachments: [ + ...(values.fileList + ? await Promise.all( + values.fileList.map(async (f) => { + return { + filename: f.name, + path: await toBase64(f.originFileObj) + }; + }) + ) + : []), + ...(pdfCopytoAttach.pdf + ? [ + { + path: pdfCopytoAttach.pdf, + filename: pdfCopytoAttach.filename && `${pdfCopytoAttach.filename}.pdf` + } + ] + : []) + ], + media: selectedMedia.filter((m) => m.isSelected).map((m) => m.fullsize) + //attachments, + }); + notification["success"]({ message: t("emails.successes.sent") }); + toggleEmailOverlayVisible(); + } catch (error) { + notification["error"]({ + message: t("emails.errors.notsent", { message: error.message }) + }); + } + setSending(false); + }; - const handleFinish = async (allValues) => { - logImEXEvent("email_send_from_modal"); + const render = async () => { + logImEXEvent("email_render_template", { template: emailConfig.template }); + setLoading(true); + let { html, pdf, filename } = await RenderTemplate(emailConfig.template, bodyshop, true); - //const attachments = []; + const response = await axios.post("/render/inlinecss", { + html: html, + url: `${window.location.protocol}://${window.location.host}/` + }); + setRawHtml(response.data); - // if (values.fileList) - // await asyncForEach(values.fileList, async (f) => { - // const t = { - // ContentType: f.type, - // Filename: f.name, - // Base64Content: (await toBase64(f.originFileObj)).split(",")[1], - // }; - // attachments.push(t); - // }); + if (pdf) { + setPdfCopytoAttach({ pdf, filename }); + } - const {from, ...values} = allValues; - setSending(true); - try { - await axios.post("/sendemail", { - bodyshopid: bodyshop.id, - jobid: emailConfig.jobid, + form.setFieldsValue({ + from: currentUser.validemail ? currentUser.email : bodyshop.email, + ...emailConfig.messageOptions, + cc: emailConfig.messageOptions.cc && emailConfig.messageOptions.cc.filter((x) => x), + to: emailConfig.messageOptions.to && emailConfig.messageOptions.to.filter((x) => x), + html: response.data, + fileList: [] + }); - ...defaultEmailFrom, - ReplyTo: { - Email: from, - Name: currentUser.displayName, - }, - ...values, - html: rawHtml, - attachments: [ - ...(values.fileList - ? await Promise.all( - values.fileList.map(async (f) => { - return { - filename: f.name, - path: await toBase64(f.originFileObj), - }; - }) - ) - : []), - ...(pdfCopytoAttach.pdf - ? [ - { - path: pdfCopytoAttach.pdf, - filename: - pdfCopytoAttach.filename && - `${pdfCopytoAttach.filename}.pdf`, - }, - ] - : []), - ], - media: selectedMedia.filter((m) => m.isSelected).map((m) => m.fullsize), - //attachments, - }); - notification["success"]({message: t("emails.successes.sent")}); - toggleEmailOverlayVisible(); - } catch (error) { - notification["error"]({ - message: t("emails.errors.notsent", {message: error.message}), - }); - } - setSending(false); - }; + if (bodyshop.md_email_cc[emailConfig.template.name] && bodyshop.md_email_cc[emailConfig.template.name].length > 0) { + form.setFieldsValue({ + cc: [...(form.getFieldValue("cc") || []), ...bodyshop.md_email_cc[emailConfig.template.name]] + }); + } + setLoading(false); + }; - const render = async () => { - logImEXEvent("email_render_template", {template: emailConfig.template}); - setLoading(true); - let {html, pdf, filename} = await RenderTemplate( - emailConfig.template, - bodyshop, - true - ); - - const response = await axios.post("/render/inlinecss", { - html: html, - url: `${window.location.protocol}://${window.location.host}/`, - }); - setRawHtml(response.data); - - if (pdf) { - setPdfCopytoAttach({pdf, filename}); - } - - form.setFieldsValue({ - from: currentUser.validemail ? currentUser.email : bodyshop.email, - ...emailConfig.messageOptions, - cc: - emailConfig.messageOptions.cc && - emailConfig.messageOptions.cc.filter((x) => x), - to: - emailConfig.messageOptions.to && - emailConfig.messageOptions.to.filter((x) => x), - html: response.data, - fileList: [], - }); - - if ( - bodyshop.md_email_cc[emailConfig.template.name] && - bodyshop.md_email_cc[emailConfig.template.name].length > 0 - ) { - form.setFieldsValue({ - cc: [ - ...(form.getFieldValue("cc") || []), - ...bodyshop.md_email_cc[emailConfig.template.name], - ], - }); - } - setLoading(false); - }; - - useEffect(() => { - if (modalVisible) render(); - }, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps - return ( - form.submit()} - title={t("emails.labels.emailpreview")} - onCancel={() => { - toggleEmailOverlayVisible(); - }} - //closeIcon={() => null} - okText={t("general.actions.send")} - okButtonProps={{ - loading: sending, - disabled: - selectedMedia && - (selectedMedia - .filter((s) => s.isSelected) - .reduce((acc, val) => (acc = acc + val.size), 0) >= - 10485760 - new Blob([form.getFieldValue("html")]).size || - selectedMedia.filter((s) => s.isSelected).length > 10), - }} + useEffect(() => { + if (modalVisible) render(); + }, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps + return ( + form.submit()} + title={t("emails.labels.emailpreview")} + onCancel={() => { + toggleEmailOverlayVisible(); + }} + //closeIcon={() => null} + okText={t("general.actions.send")} + okButtonProps={{ + loading: sending, + disabled: + selectedMedia && + (selectedMedia.filter((s) => s.isSelected).reduce((acc, val) => (acc = acc + val.size), 0) >= + 10485760 - new Blob([form.getFieldValue("html")]).size || + selectedMedia.filter((s) => s.isSelected).length > 10) + }} + > + + + + { + toggleEmailOverlayVisible(); + }} + > + {t("general.actions.cancel")} + + form.submit()} + disabled={ + selectedMedia && + (selectedMedia.filter((s) => s.isSelected).reduce((acc, val) => (acc = acc + val.size), 0) >= + 10485760 - new Blob([form.getFieldValue("html")]).size || + selectedMedia.filter((s) => s.isSelected).length > 10) + } + type="primary" + > + {t("general.actions.send")} + + + + + {loading && ( - - - { - toggleEmailOverlayVisible(); - }} - > - {t("general.actions.cancel")} - - form.submit()} - disabled={ - selectedMedia && - (selectedMedia - .filter((s) => s.isSelected) - .reduce((acc, val) => (acc = acc + val.size), 0) >= - 10485760 - new Blob([form.getFieldValue("html")]).size || - selectedMedia.filter((s) => s.isSelected).length > 10) - } - type="primary" - > - {t("general.actions.send")} - - - - - {loading && ( - - - {t("emails.labels.preview")} - - - )} - - {!loading && ( - - )} - + + {t("emails.labels.preview")} + - - ); + )} + + {!loading && } + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(EmailOverlayContainer); +export default connect(mapStateToProps, mapDispatchToProps)(EmailOverlayContainer); const toBase64 = (file) => - new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - }); + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = (error) => reject(error); + }); // const asyncForEach = async (array, callback) => { // for (let index = 0; index < array.length; index++) { diff --git a/client/src/components/email-test/email-test-component.jsx b/client/src/components/email-test/email-test-component.jsx index 122315afb..787ba3a76 100644 --- a/client/src/components/email-test/email-test-component.jsx +++ b/client/src/components/email-test/email-test-component.jsx @@ -1,115 +1,103 @@ -import {Button, Form, Input, Select, Switch} from "antd"; +import { Button, Form, Input, Select, Switch } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setEmailOptions} from "../../redux/email/email.actions"; -import {selectCurrentUser} from "../../redux/user/user.selectors"; -import {GenerateDocument} from "../../utils/RenderTemplate"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setEmailOptions } from "../../redux/email/email.actions"; +import { selectCurrentUser } from "../../redux/user/user.selectors"; +import { GenerateDocument } from "../../utils/RenderTemplate"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import dayjs from "../../utils/day"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - setEmailOptions: (e) => dispatch(setEmailOptions(e)), + setEmailOptions: (e) => dispatch(setEmailOptions(e)) }); -export function EmailTestComponent({currentUser, setEmailOptions}) { - const [form] = Form.useForm(); +export function EmailTestComponent({ currentUser, setEmailOptions }) { + const [form] = Form.useForm(); - const handleFinish = (values) => { - GenerateDocument( - { - name: values.key, - variables: { - ...(values.start - ? { - start: dayjs(values.start).startOf("day").format("YYYY-MM-DD"), - } - : {}), - ...(values.end - ? {end: dayjs(values.end).endOf("day").format("YYYY-MM-DD")} - : {}), - ...(values.start - ? {starttz: dayjs(values.start).startOf("day")} - : {}), - ...(values.end ? {endtz: dayjs(values.end).endOf("day")} : {}), + const handleFinish = (values) => { + GenerateDocument( + { + name: values.key, + variables: { + ...(values.start + ? { + start: dayjs(values.start).startOf("day").format("YYYY-MM-DD") + } + : {}), + ...(values.end ? { end: dayjs(values.end).endOf("day").format("YYYY-MM-DD") } : {}), + ...(values.start ? { starttz: dayjs(values.start).startOf("day") } : {}), + ...(values.end ? { endtz: dayjs(values.end).endOf("day") } : {}), - ...(values.id ? {id: values.id} : {}), - }, - }, - { - to: values.to, - }, - values.email ? "e" : "p" - ); - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - form.submit()}>Execute - + ...(values.id ? { id: values.id } : {}) + } + }, + { + to: values.to + }, + values.email ? "e" : "p" ); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + form.submit()}>Execute + + ); } export default connect(mapStateToProps, mapDispatchToProps)(EmailTestComponent); diff --git a/client/src/components/employee-search-select/employee-search-select.component.jsx b/client/src/components/employee-search-select/employee-search-select.component.jsx index 1388ef981..150e4b426 100644 --- a/client/src/components/employee-search-select/employee-search-select.component.jsx +++ b/client/src/components/employee-search-select/employee-search-select.component.jsx @@ -1,43 +1,37 @@ -import {Select, Space, Tag} from "antd"; +import { Select, Space, Tag } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; -const {Option} = Select; +const { Option } = Select; //To be used as a form element only. -const EmployeeSearchSelect = ({options, ...props}) => { - const {t} = useTranslation(); +const EmployeeSearchSelect = ({ options, ...props }) => { + const { t } = useTranslation(); - return ( - - {options - ? options.map((o) => ( - - - {`${o.employee_number} ${o.first_name} ${o.last_name}`} + return ( + + {options + ? options.map((o) => ( + + + {`${o.employee_number} ${o.first_name} ${o.last_name}`} - - {o.flat_rate - ? t("timetickets.labels.flat_rate") - : t("timetickets.labels.straight_time")} - - - - )) - : null} - - ); + + {o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")} + + + + )) + : null} + + ); }; export default EmployeeSearchSelect; diff --git a/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx index eef1a88cc..b59589da1 100644 --- a/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx +++ b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx @@ -1,33 +1,33 @@ -import {useQuery} from "@apollo/client"; -import {Select} from "antd"; -import React, {forwardRef} from "react"; -import {QUERY_TEAMS} from "../../graphql/employee_teams.queries"; +import { useQuery } from "@apollo/client"; +import { Select } from "antd"; +import React, { forwardRef } from "react"; +import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; import AlertComponent from "../alert/alert.component"; //To be used as a form element only. -const EmployeeTeamSearchSelect = ({...props}, ref) => { - const {loading, error, data} = useQuery(QUERY_TEAMS); +const EmployeeTeamSearchSelect = ({ ...props }, ref) => { + const { loading, error, data } = useQuery(QUERY_TEAMS); - if (error) return ; - return ( - ({ - value: JSON.stringify(e), - label: e.name, - })) - : [] - } - {...props} - /> - ); + if (error) return ; + return ( + ({ + value: JSON.stringify(e), + label: e.name + })) + : [] + } + {...props} + /> + ); }; export default forwardRef(EmployeeTeamSearchSelect); diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index d12926cf6..d4683aab3 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -1,143 +1,149 @@ -import {Button, Col, Collapse, Result, Row, Space} from "antd"; +import { Button, Col, Collapse, Result, Row, Space } from "antd"; import React from "react"; -import {withTranslation} from "react-i18next"; -import {logImEXEvent} from "../../firebase/firebase.utils"; +import { withTranslation } from "react-i18next"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import * as Sentry from "@sentry/react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); class ErrorBoundary extends React.Component { - constructor() { - super(); - this.state = { - hasErrored: false, - error: null, - info: null, - }; - } + constructor() { + super(); + this.state = { + hasErrored: false, + error: null, + info: null + }; + } - static getDerivedStateFromError(error) { - console.log("ErrorBoundary -> getDerivedStateFromError -> error", error); + static getDerivedStateFromError(error) { + console.log("ErrorBoundary -> getDerivedStateFromError -> error", error); - return {hasErrored: true, error: error}; - } + return { hasErrored: true, error: error }; + } - componentDidCatch(error, info) { - console.log("Exception Caught by Error Boundary.", error, info); - this.setState({...this.state, error, info}); - } + componentDidCatch(error, info) { + console.log("Exception Caught by Error Boundary.", error, info); + this.setState({ ...this.state, error, info }); + } - handleErrorSubmit = () => { - InstanceRenderManager({executeFunction:true,args:[], imex: () => { - window.$crisp.push([ - "do", - "message:send", - [ - "text", - `I hit the following error: \n\n + handleErrorSubmit = () => { + InstanceRenderManager({ + executeFunction: true, + args: [], + imex: () => { + window.$crisp.push([ + "do", + "message:send", + [ + "text", + `I hit the following error: \n\n ${this.state.error.message}\n\n ${this.state.error.stack}\n\n URL:${window.location} as ${this.props.currentUser.email} for ${ - this.props.bodyshop && this.props.bodyshop.name - } - `, - ], - ]); + this.props.bodyshop && this.props.bodyshop.name + } + ` + ] + ]); - window.$crisp.push(["do", "chat:open"]); - } }) - // const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue** + window.$crisp.push(["do", "chat:open"]); + } + }); + // const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue** - // ---- - // System Generated Log: - // ${this.state.error.message} - // ${this.state.error.stack} - // `; + // ---- + // System Generated Log: + // ${this.state.error.message} + // ${this.state.error.stack} + // `; - // const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI( - // errorDescription - // )}&customfield_10049=${window.location}&email=${ - // this.props.currentUser.email - // }`; - // console.log(`URL`, URL); - // window.open(URL, "_blank"); - }; + // const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI( + // errorDescription + // )}&customfield_10049=${window.location}&email=${ + // this.props.currentUser.email + // }`; + // console.log(`URL`, URL); + // window.open(URL, "_blank"); + }; - render() { - const {t} = this.props; - const {error, info} = this.state; - if (this.state.hasErrored === true) { - logImEXEvent("error_boundary_rendered", {error, info}); + render() { + const { t } = this.props; + const { error, info } = this.state; + if (this.state.hasErrored === true) { + logImEXEvent("error_boundary_rendered", { error, info }); - window.$crisp.push([ - "set", - "session:event", - [ - [ - [ - "error_boundary", - { - error: this.state.error.message, - stack: this.state.error.stack, - }, - "red", - ], - ], - ], - ]); + window.$crisp.push([ + "set", + "session:event", + [ + [ + [ + "error_boundary", + { + error: this.state.error.message, + stack: this.state.error.stack + }, + "red" + ] + ] + ] + ]); - return ( - - - { - window.location.reload(); - }} - > - {t("general.actions.refresh")} - - - {t("general.actions.senderrortosupport")} - - - } - /> - - - - - - {this.state.error.message} - - {this.state.error.stack} - - - - - - ); - } else { - return this.props.children; - } + return ( + + + { + window.location.reload(); + }} + > + {t("general.actions.refresh")} + + {t("general.actions.senderrortosupport")} + + } + /> + + + + + + {this.state.error.message} + + {this.state.error.stack} + + + + + + ); + } else { + return this.props.children; } + } } -export default Sentry.withErrorBoundary( - connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary)) -); +export default Sentry.withErrorBoundary(connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary))); diff --git a/client/src/components/eula/eula.component.jsx b/client/src/components/eula/eula.component.jsx index ec91f0b9a..6c6fb73b7 100644 --- a/client/src/components/eula/eula.component.jsx +++ b/client/src/components/eula/eula.component.jsx @@ -1,252 +1,253 @@ -import React, {useCallback, useEffect, useRef, useState} from "react"; -import {Button, Card, Checkbox, Col, Form, Input, Modal, notification, Row} from "antd"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Button, Card, Checkbox, Col, Form, Input, Modal, notification, Row } from "antd"; import Markdown from "react-markdown"; -import {createStructuredSelector} from "reselect"; -import {selectCurrentEula, selectCurrentUser} from "../../redux/user/user.selectors"; -import {connect} from "react-redux"; -import {FormDatePicker} from "../form-date-picker/form-date-picker.component"; -import {INSERT_EULA_ACCEPTANCE} from "../../graphql/user.queries"; -import {useMutation} from "@apollo/client"; -import {acceptEula} from "../../redux/user/user.actions"; -import {useTranslation} from "react-i18next"; -import day from '../../utils/day'; +import { createStructuredSelector } from "reselect"; +import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { FormDatePicker } from "../form-date-picker/form-date-picker.component"; +import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries"; +import { useMutation } from "@apollo/client"; +import { acceptEula } from "../../redux/user/user.actions"; +import { useTranslation } from "react-i18next"; +import day from "../../utils/day"; -import './eula.styles.scss'; +import "./eula.styles.scss"; -const Eula = ({currentEula, currentUser, acceptEula}) => { - const [formReady, setFormReady] = useState(false); - const [hasEverScrolledToBottom, setHasEverScrolledToBottom] = useState(false); - const [insertEulaAcceptance] = useMutation(INSERT_EULA_ACCEPTANCE); - const [form] = Form.useForm(); - const markdownCardRef = useRef(null); - const {t} = useTranslation(); - const [api, contextHolder] = notification.useNotification(); +const Eula = ({ currentEula, currentUser, acceptEula }) => { + const [formReady, setFormReady] = useState(false); + const [hasEverScrolledToBottom, setHasEverScrolledToBottom] = useState(false); + const [insertEulaAcceptance] = useMutation(INSERT_EULA_ACCEPTANCE); + const [form] = Form.useForm(); + const markdownCardRef = useRef(null); + const { t } = useTranslation(); + const [api, contextHolder] = notification.useNotification(); - const handleScroll = useCallback((e) => { - const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight; - if (bottom && !hasEverScrolledToBottom) { - setHasEverScrolledToBottom(true); - } else if (e.target.scrollHeight <= e.target.clientHeight && !hasEverScrolledToBottom) { - setHasEverScrolledToBottom(true); + const handleScroll = useCallback( + (e) => { + const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight; + if (bottom && !hasEverScrolledToBottom) { + setHasEverScrolledToBottom(true); + } else if (e.target.scrollHeight <= e.target.clientHeight && !hasEverScrolledToBottom) { + setHasEverScrolledToBottom(true); + } + }, + [hasEverScrolledToBottom, setHasEverScrolledToBottom] + ); + + useEffect(() => { + handleScroll({ target: markdownCardRef.current }); + }, [handleScroll]); + + const handleChange = useCallback(() => { + form + .validateFields({ validateOnly: true }) + .then(() => setFormReady(hasEverScrolledToBottom)) + .catch(() => setFormReady(false)); + }, [form, hasEverScrolledToBottom]); + + useEffect(() => { + handleChange(); + }, [handleChange, hasEverScrolledToBottom, form]); + + const onFinish = async ({ acceptTerms, ...formValues }) => { + const eulaId = currentEula.id; + const useremail = currentUser.email; + + try { + const { accepted_terms, ...otherFormValues } = formValues; + + // Trim the values of the fields before submitting + const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => { + acc[key] = typeof value === "string" ? value.trim() : value; + return acc; + }, {}); + + await insertEulaAcceptance({ + variables: { + eulaAcceptance: { + eulaid: eulaId, + useremail, + ...otherFormValues, + ...trimmedFormValues, + date_accepted: new Date() + } } - }, [hasEverScrolledToBottom, setHasEverScrolledToBottom]); + }); + acceptEula(); + } catch (err) { + api.error({ + message: t("eula.errors.acceptance.message"), + description: t("eula.errors.acceptance.description"), + placement: "bottomRight", + duration: 5000 + }); + console.log(`${t("eula.errors.acceptance.message")}`); + console.dir({ + message: err.message, + stack: err.stack + }); + } + }; - useEffect(() => { - handleScroll({target: markdownCardRef.current}); - }, [handleScroll]); + return ( + <> + {contextHolder} + ( + + )} + closable={false} + > + + + + + - const handleChange = useCallback(() => { - form.validateFields({validateOnly: true}) - .then(() => setFormReady(hasEverScrolledToBottom)) - .catch(() => setFormReady(false)); - }, [form, hasEverScrolledToBottom]); + - useEffect(() => { - handleChange(); - }, [handleChange, hasEverScrolledToBottom, form]); + {!hasEverScrolledToBottom && ( + + {t("eula.content.never_scrolled")} + + )} + + > + ); +}; - const onFinish = async ({acceptTerms, ...formValues}) => { - const eulaId = currentEula.id; - const useremail = currentUser.email; - - try { - const {accepted_terms, ...otherFormValues} = formValues; - - // Trim the values of the fields before submitting - const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => { - acc[key] = typeof value === 'string' ? value.trim() : value; - return acc; - }, {}); - - await insertEulaAcceptance({ - variables: { - eulaAcceptance: { - eulaid: eulaId, - useremail, - ...otherFormValues, - ...trimmedFormValues, - date_accepted: new Date(), - } +const EulaFormComponent = ({ form, handleChange, onFinish, t }) => ( + + + + + + value.trim() !== "" ? Promise.resolve() : Promise.reject(new Error(t("eula.messages.first_name"))) + } + ]} + > + + + + + + value.trim() !== "" ? Promise.resolve() : Promise.reject(new Error(t("eula.messages.last_name"))) + } + ]} + > + + + + + + + + value.trim() !== "" ? Promise.resolve() : Promise.reject(new Error(t("eula.messages.business_name"))) + } + ]} + > + + + + + + + + + + + + + + + + + { + if (day(value).isSame(day(), "day")) { + return Promise.resolve(); + } + return Promise.reject(new Error(t("eula.messages.date_accepted"))); } - }); - acceptEula(); - } catch (err) { - api.error({ - message: t('eula.errors.acceptance.message'), - description: t('eula.errors.acceptance.description'), - placement: 'bottomRight', - duration: 5000, - - }); - console.log(`${t('eula.errors.acceptance.message')}`); - console.dir({ - message: err.message, - stack: err.stack, - }); - } - }; - - return ( - <> - {contextHolder} - ( - - )} - closable={false} - > - - - - - - - - - {!hasEverScrolledToBottom && ( - - {t('eula.content.never_scrolled')} - - )} - - > - ) -} - -const EulaFormComponent = ({form, handleChange, onFinish, t}) => ( - - - - - - value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.first_name'))), - },]} - > - - - - - - value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.last_name'))), - }]} - > - - - - - - - - value.trim() !== '' ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.business_name'))), - }]} - > - - - - - - - - - - - - - - - - - { - if (day(value).isSame(day(), 'day')) { - return Promise.resolve(); - } - return Promise.reject(new Error(t('eula.messages.date_accepted'))); - } - }, - ]} - > - - - - - - - - value ? Promise.resolve() : Promise.reject(new Error(t('eula.messages.accepted_terms'))), - }, - ]} - > - {t('eula.labels.accepted_terms')} - - - - - + } + ]} + > + + + + + + + + value ? Promise.resolve() : Promise.reject(new Error(t("eula.messages.accepted_terms"))) + } + ]} + > + {t("eula.labels.accepted_terms")} + + + + + ); const mapStateToProps = createStructuredSelector({ - currentEula: selectCurrentEula, - currentUser: selectCurrentUser, + currentEula: selectCurrentEula, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - acceptEula: () => dispatch(acceptEula()), + acceptEula: () => dispatch(acceptEula()) }); -export default connect(mapStateToProps, mapDispatchToProps)(Eula); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Eula); diff --git a/client/src/components/eula/eula.styles.scss b/client/src/components/eula/eula.styles.scss index 6e12e7032..1773ffa7a 100644 --- a/client/src/components/eula/eula.styles.scss +++ b/client/src/components/eula/eula.styles.scss @@ -18,4 +18,4 @@ .eula-accept-button { width: 100%; -} \ No newline at end of file +} diff --git a/client/src/components/export-logs-count-display/export-logs-count-display.component.jsx b/client/src/components/export-logs-count-display/export-logs-count-display.component.jsx index 2fe64c8a2..df8a03a94 100644 --- a/client/src/components/export-logs-count-display/export-logs-count-display.component.jsx +++ b/client/src/components/export-logs-count-display/export-logs-count-display.component.jsx @@ -1,25 +1,25 @@ import React from "react"; -import {WarningOutlined} from "@ant-design/icons"; -import {Space, Tooltip} from "antd"; -import {useTranslation} from "react-i18next"; +import { WarningOutlined } from "@ant-design/icons"; +import { Space, Tooltip } from "antd"; +import { useTranslation } from "react-i18next"; const style = { - fontWeight: "bold", - color: "green", + fontWeight: "bold", + color: "green" }; -export default function ExportLogsCountDisplay({logs}) { - const success = logs.filter((e) => e.successful).length; - const attempts = logs.length; - const {t} = useTranslation(); - return ( - 0 ? style : {}}> - {`${success}/${attempts}`} - {success > 0 && ( - - - - )} - - ); +export default function ExportLogsCountDisplay({ logs }) { + const success = logs.filter((e) => e.successful).length; + const attempts = logs.length; + const { t } = useTranslation(); + return ( + 0 ? style : {}}> + {`${success}/${attempts}`} + {success > 0 && ( + + + + )} + + ); } diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx index f249bfa37..0b03a7d7b 100644 --- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx +++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx @@ -1,14 +1,14 @@ -import dayjs from '../../utils/day'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; -import { createStructuredSelector } from 'reselect'; -import { selectBodyshop } from '../../redux/user/user.selectors'; -import AlertComponent from '../alert/alert.component'; -import InstanceRenderManager from '../../utils/instanceRenderMgr'; +import dayjs from "../../utils/day"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps }) { @@ -19,12 +19,12 @@ function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps return ( noauth || ( diff --git a/client/src/components/form-date-picker/form-date-picker.component.jsx b/client/src/components/form-date-picker/form-date-picker.component.jsx index 4e1c50f7f..500e22d45 100644 --- a/client/src/components/form-date-picker/form-date-picker.component.jsx +++ b/client/src/components/form-date-picker/form-date-picker.component.jsx @@ -1,117 +1,123 @@ -import {DatePicker} from "antd"; +import { DatePicker } from "antd"; import dayjs from "../../utils/day"; -import React, {useRef} from "react"; +import React, { useRef } from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker); const dateFormat = "MM/DD/YYYY"; export function FormDatePicker({ - bodyshop, - value, - onChange, - onBlur, - onlyFuture, - onlyToday, - isDateOnly = true, - ...restProps - }) { - const ref = useRef(); + bodyshop, + value, + onChange, + onBlur, + onlyFuture, + onlyToday, + isDateOnly = true, + ...restProps +}) { + const ref = useRef(); - const handleChange = (newDate) => { - if (value !== newDate && onChange) { - onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate); + const handleChange = (newDate) => { + if (value !== newDate && onChange) { + onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate); + } + }; + + const handleKeyDown = (e) => { + if (e.key.toLowerCase() === "t") { + if (onChange) { + onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs()); + } + } else if (e.key.toLowerCase() === "enter") { + if (ref.current && ref.current.blur) ref.current.blur(); + } + }; + + const handleBlur = (e) => { + const v = e.target.value; + if (!v) return; + + const formats = [ + "MMDDYY", + "MMDDYYYY", + "MM/DD/YY", + "MM/DD/YYYY", + "M/DD/YY", + "M/DD/YYYY", + "MM/D/YY", + "MM/D/YYYY", + "M/D/YY", + "M/D/YYYY", + "D/MM/YY", + "D/MM/YYYY", + "DD/M/YY", + "DD/M/YYYY", + "D/M/YY", + "D/M/YYYY" + ]; + + let _a; + + // Iterate through formats to find the correct one + for (let format of formats) { + _a = dayjs(v, format); + if (v === _a.format(format)) { + break; + } + } + + if (_a.isValid() && value && value.isValid && value.isValid()) { + _a.set({ + hours: value.hours(), + minutes: value.minutes(), + seconds: value.seconds(), + milliseconds: value.milliseconds() + }); + } + + if (_a.isValid() && onChange) { + if (onlyFuture) { + if (dayjs().subtract(1, "day").isBefore(_a)) { + onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a); + } else { + onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs()); } - }; + } else { + onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a); + } + } + }; - const handleKeyDown = (e) => { - if (e.key.toLowerCase() === "t") { - if (onChange) { - onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs()); - } - } else if (e.key.toLowerCase() === "enter") { - if (ref.current && ref.current.blur) ref.current.blur(); - } - }; - - const handleBlur = (e) => { - const v = e.target.value; - if (!v) return; - - const formats = [ - "MMDDYY", "MMDDYYYY", "MM/DD/YY", "MM/DD/YYYY", - "M/DD/YY", "M/DD/YYYY", - "MM/D/YY", "MM/D/YYYY", "M/D/YY", "M/D/YYYY", - "D/MM/YY", "D/MM/YYYY", - "DD/M/YY", "DD/M/YYYY", "D/M/YY", "D/M/YYYY" - ]; - - let _a; - - // Iterate through formats to find the correct one - for (let format of formats) { - _a = dayjs(v, format); - if (v === _a.format(format)) { - break; - } - } - - if ( - _a.isValid() - && value - && value.isValid - && value.isValid() - ) { - _a.set({ - hours: value.hours(), - minutes: value.minutes(), - seconds: value.seconds(), - milliseconds: value.milliseconds(), - }); - } - - if (_a.isValid() && onChange) { - if (onlyFuture) { - if (dayjs().subtract(1, "day").isBefore(_a)) { - onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a); - } else { - onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs()); - } - } else { - onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a); - } - } - }; - - return ( - - { - if (onlyToday) { - return !dayjs().isSame(d, 'day'); - } else if (onlyFuture) { - return dayjs().subtract(1, "day").isAfter(d); - } - }} - {...restProps} - /> - - ); -} \ No newline at end of file + return ( + + { + if (onlyToday) { + return !dayjs().isSame(d, "day"); + } else if (onlyFuture) { + return dayjs().subtract(1, "day").isAfter(d); + } + }} + {...restProps} + /> + + ); +} diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index e5a498ca8..8c11ece9d 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -1,49 +1,46 @@ -import React, {forwardRef} from "react"; +import React, { forwardRef } from "react"; //import DatePicker from "react-datepicker"; //import "react-datepicker/src/stylesheets/datepicker.scss"; -import {TimePicker} from "antd"; +import { TimePicker } from "antd"; import dayjs from "../../utils/day"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; //To be used as a form element only. -const DateTimePicker = ( - {value, onChange, onBlur, id, onlyFuture, ...restProps}, - ref -) => { - // const handleChange = (newDate) => { - // if (value !== newDate && onChange) { - // onChange(newDate); - // } - // }; +const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps }, ref) => { + // const handleChange = (newDate) => { + // if (value !== newDate && onChange) { + // onChange(newDate); + // } + // }; - return ( - - dayjs().subtract(1, "day").isAfter(d), - })} - value={value} - onBlur={onBlur} - onChange={onChange} - onlyFuture={onlyFuture} - isDateOnly={false} - /> + return ( + + dayjs().subtract(1, "day").isAfter(d) + })} + value={value} + onBlur={onBlur} + onChange={onChange} + onlyFuture={onlyFuture} + isDateOnly={false} + /> - dayjs().isAfter(d), - })} - onChange={onChange} - disableSeconds={true} - minuteStep={15} - onBlur={onBlur} - format="hh:mm a" - {...restProps} - /> - - ); + dayjs().isAfter(d) + })} + onChange={onChange} + disableSeconds={true} + minuteStep={15} + onBlur={onBlur} + format="hh:mm a" + {...restProps} + /> + + ); }; export default forwardRef(DateTimePicker); diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx index 1d5576720..502d1bbf9 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx +++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx @@ -1,70 +1,56 @@ import React from "react"; -import {Form, Space} from "antd"; -import {useTranslation} from "react-i18next"; +import { Form, Space } from "antd"; +import { useTranslation } from "react-i18next"; import AlertComponent from "../alert/alert.component"; import "./form-fields-changed.styles.scss"; import Prompt from "../../utils/prompt"; -export default function FormsFieldChanged({form, skipPrompt}) { - const {t} = useTranslation(); +export default function FormsFieldChanged({ form, skipPrompt }) { + const { t } = useTranslation(); - const handleReset = () => { - form.resetFields(); - }; - //if (!form.isFieldsTouched()) return <>>; - return ( - - {() => { - const errors = form.getFieldsError().filter((e) => e.errors.length > 0); - if (form.isFieldsTouched()) - return ( - - - - {t("general.messages.unsavedchanges")} - + const handleReset = () => { + form.resetFields(); + }; + //if (!form.isFieldsTouched()) return <>>; + return ( + + {() => { + const errors = form.getFieldsError().filter((e) => e.errors.length > 0); + if (form.isFieldsTouched()) + return ( + + + + {t("general.messages.unsavedchanges")} + {t("general.actions.reset")} - - } - /> - {errors.length > 0 && ( - - - {errors.map((e, idx) => - e.errors.map((e2, idx2) => ( - {e2} - )) - )} - - - } - /> - )} - - ); - return ; - }} - - ); + + } + /> + {errors.length > 0 && ( + + {errors.map((e, idx) => e.errors.map((e2, idx2) => {e2}))} + + } + /> + )} + + ); + return ; + }} + + ); } diff --git a/client/src/components/form-input-number-calculator/form-input-number-calculator.component.jsx b/client/src/components/form-input-number-calculator/form-input-number-calculator.component.jsx index 9f73becaf..5040fd909 100644 --- a/client/src/components/form-input-number-calculator/form-input-number-calculator.component.jsx +++ b/client/src/components/form-input-number-calculator/form-input-number-calculator.component.jsx @@ -1,111 +1,108 @@ -import {InputNumber, Popover} from "antd"; -import React, {forwardRef, useEffect, useRef, useState} from "react"; +import { InputNumber, Popover } from "antd"; +import React, { forwardRef, useEffect, useRef, useState } from "react"; -const FormInputNUmberCalculator = ( - {value: formValue, onChange: formOnChange, ...restProps}, - refProp -) => { - const [value, setValue] = useState(formValue); - const [total, setTotal] = useState(0); - const [history, setHistory] = useState([]); +const FormInputNUmberCalculator = ({ value: formValue, onChange: formOnChange, ...restProps }, refProp) => { + const [value, setValue] = useState(formValue); + const [total, setTotal] = useState(0); + const [history, setHistory] = useState([]); - const ref = useRef(null); + const ref = useRef(null); - const handleKeyDown = (e) => { - const {key} = e; - let action; - switch (key) { - case "/": - case "*": - case "+": - case "-": - action = key; - break; - case "Enter": - action = "="; - break; - default: - setValue(e.currentTarget.value); - return; + const handleKeyDown = (e) => { + const { key } = e; + let action; + switch (key) { + case "/": + case "*": + case "+": + case "-": + action = key; + break; + case "Enter": + action = "="; + break; + default: + setValue(e.currentTarget.value); + return; + } + const val = parseFloat(value); + setValue(null); + ref.current.blur(); + ref.current.focus(); + if (!isNaN(val)) { + setHistory([...history, val, action]); + } + }; + + useEffect(() => { + if (value !== formValue && formOnChange) formOnChange(value); + }, [formOnChange, formValue, value]); + + useEffect(() => { + if (history.length > 2) { + const subTotal = history.reduce((acc, val, idx) => { + if (idx === 0) { + return val; } - const val = parseFloat(value); - setValue(null); - ref.current.blur(); - ref.current.focus(); - if (!isNaN(val)) { - setHistory([...history, val, action]); - } - }; + switch (val) { + case "/": + case "*": + case "+": + case "-": + return acc; - useEffect(() => { - if (value !== formValue && formOnChange) formOnChange(value); - }, [formOnChange, formValue, value]); - - useEffect(() => { - if (history.length > 2) { - const subTotal = history.reduce((acc, val, idx) => { - if (idx === 0) { - return val; - } - switch (val) { - case "/": - case "*": - case "+": - case "-": - return acc; - - default: - //Weve got math on our hands. Find the last operator, and apply it accordingly. - switch (history[idx - 1]) { - case "/": - return acc / val; - case "*": - return acc * val; - case "+": - return acc + val; - case "-": - return acc - val; - default: - return acc; - } - } - }, 0); - setTotal(subTotal); - if (history[history.length - 1] === "=") { - setValue(subTotal); - ref.current.blur(); - ref.current.focus(); - setHistory([]); + default: + //Weve got math on our hands. Find the last operator, and apply it accordingly. + switch (history[idx - 1]) { + case "/": + return acc / val; + case "*": + return acc * val; + case "+": + return acc + val; + case "-": + return acc - val; + default: + return acc; } } - }, [history]); + }, 0); + setTotal(subTotal); + if (history[history.length - 1] === "=") { + setValue(subTotal); + ref.current.blur(); + ref.current.focus(); + setHistory([]); + } + } + }, [history]); - const popContent = ( - - History - {history.map((h, idx) => ( - - {h} - - ))} - {total} + const popContent = ( + + History + {history.map((h, idx) => ( + + {h} - ); + ))} + {total} + + ); - return ( - - 0}> - setHistory([])} - {...restProps} - /> - - - ); + return ( + + 0}> + setHistory([])} + {...restProps} + /> + + + ); }; export default forwardRef(FormInputNUmberCalculator); diff --git a/client/src/components/form-items-formatted/colorpicker-form-item.component.jsx b/client/src/components/form-items-formatted/colorpicker-form-item.component.jsx index cd7c1bc16..10734e291 100644 --- a/client/src/components/form-items-formatted/colorpicker-form-item.component.jsx +++ b/client/src/components/form-items-formatted/colorpicker-form-item.component.jsx @@ -1,20 +1,20 @@ import React from "react"; -import {SliderPicker} from "react-color"; +import { SliderPicker } from "react-color"; //To be used as a form element only. -const ColorPickerFormItem = ({value, onChange, style, ...restProps}) => { - const handleChangeComplete = (color) => { - if (onChange) onChange(color); - }; +const ColorPickerFormItem = ({ value, onChange, style, ...restProps }) => { + const handleChangeComplete = (color) => { + if (onChange) onChange(color); + }; - return ( - - ); + return ( + + ); }; export default ColorPickerFormItem; diff --git a/client/src/components/form-items-formatted/currency-form-item.component.jsx b/client/src/components/form-items-formatted/currency-form-item.component.jsx index 3e45c2ff1..8d142372d 100644 --- a/client/src/components/form-items-formatted/currency-form-item.component.jsx +++ b/client/src/components/form-items-formatted/currency-form-item.component.jsx @@ -1,5 +1,5 @@ -import {InputNumber} from "antd"; -import React, {forwardRef} from "react"; +import { InputNumber } from "antd"; +import React, { forwardRef } from "react"; // const locale = "en-us"; // const currencyFormatter = (value) => { @@ -42,16 +42,16 @@ import React, {forwardRef} from "react"; // }; function FormItemCurrency(props, ref) { - return ( - `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")} - // parser={(value) => value.replace(/\$\s?|(,*)/g, "")} - precision={2} - /> - ); + return ( + `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + // parser={(value) => value.replace(/\$\s?|(,*)/g, "")} + precision={2} + /> + ); } export default forwardRef(FormItemCurrency); diff --git a/client/src/components/form-items-formatted/email-form-item.component.jsx b/client/src/components/form-items-formatted/email-form-item.component.jsx index 5d5db588e..c2a018d26 100644 --- a/client/src/components/form-items-formatted/email-form-item.component.jsx +++ b/client/src/components/form-items-formatted/email-form-item.component.jsx @@ -1,23 +1,23 @@ -import {MailFilled} from "@ant-design/icons"; -import {Input} from "antd"; -import React, {forwardRef} from "react"; +import { MailFilled } from "@ant-design/icons"; +import { Input } from "antd"; +import React, { forwardRef } from "react"; function FormItemEmail(props, ref) { - return ( - - - - ) : ( - - ) - } - /> - ); + return ( + + + + ) : ( + + ) + } + /> + ); } export default forwardRef(FormItemEmail); diff --git a/client/src/components/form-items-formatted/labor-type-form-item.component.jsx b/client/src/components/form-items-formatted/labor-type-form-item.component.jsx index 6066a1133..f1f4d8265 100644 --- a/client/src/components/form-items-formatted/labor-type-form-item.component.jsx +++ b/client/src/components/form-items-formatted/labor-type-form-item.component.jsx @@ -1,11 +1,11 @@ -import React, {forwardRef} from "react"; -import {useTranslation} from "react-i18next"; +import React, { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; -const LaborTypeFormItem = ({value, onChange}, ref) => { - const {t} = useTranslation(); +const LaborTypeFormItem = ({ value, onChange }, ref) => { + const { t } = useTranslation(); - if (!value) return null; + if (!value) return null; - return {t(`joblines.fields.lbr_types.${value}`)}; + return {t(`joblines.fields.lbr_types.${value}`)}; }; export default forwardRef(LaborTypeFormItem); diff --git a/client/src/components/form-items-formatted/part-type-form-item.component.jsx b/client/src/components/form-items-formatted/part-type-form-item.component.jsx index 8aea0d9aa..3226483ca 100644 --- a/client/src/components/form-items-formatted/part-type-form-item.component.jsx +++ b/client/src/components/form-items-formatted/part-type-form-item.component.jsx @@ -1,11 +1,11 @@ -import React, {forwardRef} from "react"; -import {useTranslation} from "react-i18next"; +import React, { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; -const PartTypeFormItem = ({value, onChange}, ref) => { - const {t} = useTranslation(); +const PartTypeFormItem = ({ value, onChange }, ref) => { + const { t } = useTranslation(); - if (!value) return null; + if (!value) return null; - return {t(`joblines.fields.part_types.${value}`)}; + return {t(`joblines.fields.part_types.${value}`)}; }; export default forwardRef(PartTypeFormItem); diff --git a/client/src/components/form-items-formatted/phone-form-item.component.jsx b/client/src/components/form-items-formatted/phone-form-item.component.jsx index b8f4d38b1..86c0179b6 100644 --- a/client/src/components/form-items-formatted/phone-form-item.component.jsx +++ b/client/src/components/form-items-formatted/phone-form-item.component.jsx @@ -1,31 +1,31 @@ -import {Input} from "antd"; +import { Input } from "antd"; import i18n from "i18next"; import parsePhoneNumber from "libphonenumber-js"; -import React, {forwardRef} from "react"; +import React, { forwardRef } from "react"; import "./phone-form-item.styles.scss"; function FormItemPhone(props, ref) { - return ( - - ); + return ( + + ); } export default forwardRef(FormItemPhone); export const PhoneItemFormatterValidation = (getFieldValue, name) => ({ - async validator(rule, value) { - if (!value) { - return Promise.resolve(); - } else { - const p = parsePhoneNumber(value, "CA"); - if (p && p.isValid()) { - return Promise.resolve(); - } else { - return Promise.reject(i18n.t("general.validation.invalidphone")); - } - } - }, + async validator(rule, value) { + if (!value) { + return Promise.resolve(); + } else { + const p = parsePhoneNumber(value, "CA"); + if (p && p.isValid()) { + return Promise.resolve(); + } else { + return Promise.reject(i18n.t("general.validation.invalidphone")); + } + } + } }); diff --git a/client/src/components/form-items-formatted/read-only-form-item.component.jsx b/client/src/components/form-items-formatted/read-only-form-item.component.jsx index e606fb3aa..09c6ef6e9 100644 --- a/client/src/components/form-items-formatted/read-only-form-item.component.jsx +++ b/client/src/components/form-items-formatted/read-only-form-item.component.jsx @@ -1,39 +1,31 @@ import Dinero from "dinero.js"; -import React, {forwardRef} from "react"; +import React, { forwardRef } from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -const ReadOnlyFormItem = ( - {bodyshop, value, type = "text", onChange}, - ref -) => { - if (!value) return null; - switch (type) { - case "employee": - const emp = bodyshop.employees.find((e) => e.id === value); - return `${emp?.first_name} ${emp?.last_name}`; +const ReadOnlyFormItem = ({ bodyshop, value, type = "text", onChange }, ref) => { + if (!value) return null; + switch (type) { + case "employee": + const emp = bodyshop.employees.find((e) => e.id === value); + return `${emp?.first_name} ${emp?.last_name}`; - case "text": - return {value}; - case "currency": - return ( - {Dinero({amount: Math.round(value * 100)}).toFormat()} - ); - default: - return {value}; - } + case "text": + return {value}; + case "currency": + return {Dinero({ amount: Math.round(value * 100) }).toFormat()}; + default: + return {value}; + } }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(forwardRef(ReadOnlyFormItem)); +export default connect(mapStateToProps, mapDispatchToProps)(forwardRef(ReadOnlyFormItem)); diff --git a/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx b/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx index 44a5980c3..cd3c4fe5a 100644 --- a/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx +++ b/client/src/components/form-list-move-arrows/form-list-move-arrows.component.jsx @@ -1,23 +1,23 @@ -import {DownOutlined, UpOutlined} from "@ant-design/icons"; -import {Space} from "antd"; +import { DownOutlined, UpOutlined } from "@ant-design/icons"; +import { Space } from "antd"; import React from "react"; -export default function FormListMoveArrows({move, index, total}) { - const upDisabled = index === 0; - const downDisabled = index === total - 1; +export default function FormListMoveArrows({ move, index, total }) { + const upDisabled = index === 0; + const downDisabled = index === total - 1; - const handleUp = () => { - move(index, index - 1); - }; + const handleUp = () => { + move(index, index - 1); + }; - const handleDown = () => { - move(index, index + 1); - }; + const handleDown = () => { + move(index, index + 1); + }; - return ( - - - - - ); + return ( + + + + + ); } diff --git a/client/src/components/global-loading-bar/global-loading-bar.component.jsx b/client/src/components/global-loading-bar/global-loading-bar.component.jsx index d59535512..833cae896 100644 --- a/client/src/components/global-loading-bar/global-loading-bar.component.jsx +++ b/client/src/components/global-loading-bar/global-loading-bar.component.jsx @@ -1,54 +1,54 @@ //import { useNProgress } from "@tanem/react-nprogress"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectLoading} from "../../redux/application/application.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectLoading } from "../../redux/application/application.selectors"; const mapStateToProps = createStructuredSelector({ - loading: selectLoading, + loading: selectLoading }); export default connect(mapStateToProps, null)(GlobalLoadingHeader); -function GlobalLoadingHeader({loading}) { - return ; - // const { animationDuration, isFinished, progress } = useNProgress({ - // isAnimating: loading, - // }); - // return ( - // - // - // - // - // - // ); +function GlobalLoadingHeader({ loading }) { + return ; + // const { animationDuration, isFinished, progress } = useNProgress({ + // isAnimating: loading, + // }); + // return ( + // + // + // + // + // + // ); } diff --git a/client/src/components/global-search/global-search-os.component.jsx b/client/src/components/global-search/global-search-os.component.jsx index 7174b4f94..62e44da5f 100644 --- a/client/src/components/global-search/global-search-os.component.jsx +++ b/client/src/components/global-search/global-search-os.component.jsx @@ -1,215 +1,199 @@ -import {AutoComplete, Divider, Input, Space} from "antd"; +import { AutoComplete, Divider, Input, Space } from "antd"; import axios from "axios"; import _ from "lodash"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {Link, useNavigate} from "react-router-dom"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; -import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; export default function GlobalSearchOs() { - const {t} = useTranslation(); - const history = useNavigate(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState(false); + const { t } = useTranslation(); + const history = useNavigate(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(false); - const executeSearch = async (v) => { - if (v && v && v !== "" && v.length >= 3) { - try { - setLoading(true); - const searchData = await axios.post("/search", { - search: v, - }); + const executeSearch = async (v) => { + if (v && v && v !== "" && v.length >= 3) { + try { + setLoading(true); + const searchData = await axios.post("/search", { + search: v + }); - const resultsByType = { - payments: [], - jobs: [], - bills: [], - owners: [], - vehicles: [], - }; + const resultsByType = { + payments: [], + jobs: [], + bills: [], + owners: [], + vehicles: [] + }; - searchData.data.hits.hits.forEach((hit) => { - resultsByType[hit._index].push(hit._source); - }); - setData([ - { - label: renderTitle(t("menus.header.search.jobs")), - options: resultsByType.jobs.map((job) => { - return { - key: job.id, - value: job.ro_number || "N/A", - label: ( - - }> - {job.ro_number || t("general.labels.na")} - {`${job.status || ""}`} - - - - {`${job.v_model_yr || ""} ${ - job.v_make_desc || "" - } ${job.v_model_desc || ""}`} - {`${job.clm_no || ""}`} - {`${job.plate_no || ""}`} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.owners")), - options: resultsByType.owners.map((owner) => { - return { - key: owner.id, - value: OwnerNameDisplayFunction(owner), - label: ( - - } - wrap - > + searchData.data.hits.hits.forEach((hit) => { + resultsByType[hit._index].push(hit._source); + }); + setData([ + { + label: renderTitle(t("menus.header.search.jobs")), + options: resultsByType.jobs.map((job) => { + return { + key: job.id, + value: job.ro_number || "N/A", + label: ( + + }> + {job.ro_number || t("general.labels.na")} + {`${job.status || ""}`} - + - - {owner.ownr_ph1} - - - {owner.ownr_ph2} - - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.vehicles")), - options: resultsByType.vehicles.map((vehicle) => { - return { - key: vehicle.id, - value: `${vehicle.v_model_yr || ""} ${ - vehicle.v_make_desc || "" - } ${vehicle.v_model_desc || ""}`, - label: ( - - }> + {`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`} + {`${job.clm_no || ""}`} + {`${job.plate_no || ""}`} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.owners")), + options: resultsByType.owners.map((owner) => { + return { + key: owner.id, + value: OwnerNameDisplayFunction(owner), + label: ( + + } wrap> - {`${vehicle.v_model_yr || ""} ${ - vehicle.v_make_desc || "" - } ${vehicle.v_model_desc || ""}`} + - {vehicle.plate_no || ""} - - - {vehicle.v_vin || ""} - + {owner.ownr_ph1} + {owner.ownr_ph2} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.vehicles")), + options: resultsByType.vehicles.map((vehicle) => { + return { + key: vehicle.id, + value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, + label: ( + + }> + + {`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.payments")), - options: resultsByType.payments.map((payment) => { - return { - key: payment.id, - value: `${payment.job?.ro_number} ${payment.amount}`, - label: ( - - }> - {payment.paymentnum} - {payment.job?.ro_number} - {payment.memo || ""} - {payment.amount || ""} - {payment.transactionid || ""} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.bills")), - options: resultsByType.bills.map((bill) => { - return { - key: bill.id, - value: `${bill.invoice_number} - ${bill.vendor.name}`, - label: ( - - }> - {bill.invoice_number} - {bill.vendor.name} - {bill.date} - - - ), - }; - }), - }, - // { - // label: renderTitle(t("menus.header.search.phonebook")), - // options: resultsByType.search_phonebook.map((pb) => { - // return { - // key: pb.id, - // value: `${pb.firstname || ""} ${pb.lastname || ""} ${ - // pb.company || "" - // }`, - // label: ( - // - // }> - // {`${pb.firstname || ""} ${pb.lastname || ""} ${ - // pb.company || "" - // }`} - // {pb.phone1} - // {pb.email} - // - // - // ), - // }; - // }), - // }, - ]); - } catch (error) { - console.log("Error while fetching search results", error); - } finally { - setLoading(false); - } - } - }; - const debouncedExecuteSearch = _.debounce(executeSearch, 750); + {vehicle.plate_no || ""} + + {vehicle.v_vin || ""} + + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.payments")), + options: resultsByType.payments.map((payment) => { + return { + key: payment.id, + value: `${payment.job?.ro_number} ${payment.amount}`, + label: ( + + }> + {payment.paymentnum} + {payment.job?.ro_number} + {payment.memo || ""} + {payment.amount || ""} + {payment.transactionid || ""} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.bills")), + options: resultsByType.bills.map((bill) => { + return { + key: bill.id, + value: `${bill.invoice_number} - ${bill.vendor.name}`, + label: ( + + }> + {bill.invoice_number} + {bill.vendor.name} + {bill.date} + + + ) + }; + }) + } + // { + // label: renderTitle(t("menus.header.search.phonebook")), + // options: resultsByType.search_phonebook.map((pb) => { + // return { + // key: pb.id, + // value: `${pb.firstname || ""} ${pb.lastname || ""} ${ + // pb.company || "" + // }`, + // label: ( + // + // }> + // {`${pb.firstname || ""} ${pb.lastname || ""} ${ + // pb.company || "" + // }`} + // {pb.phone1} + // {pb.email} + // + // + // ), + // }; + // }), + // }, + ]); + } catch (error) { + console.log("Error while fetching search results", error); + } finally { + setLoading(false); + } + } + }; + const debouncedExecuteSearch = _.debounce(executeSearch, 750); - const handleSearch = (value) => { - debouncedExecuteSearch(value); - }; + const handleSearch = (value) => { + debouncedExecuteSearch(value); + }; - const renderTitle = (title) => { - return {title}; - }; + const renderTitle = (title) => { + return {title}; + }; - return ( - { - history(opt.label.props.to); - }} - onClear={() => setData([])} - > - - - ); + return ( + { + history(opt.label.props.to); + }} + onClear={() => setData([])} + > + + + ); } diff --git a/client/src/components/global-search/global-search.component.jsx b/client/src/components/global-search/global-search.component.jsx index 036463522..9bb0da845 100644 --- a/client/src/components/global-search/global-search.component.jsx +++ b/client/src/components/global-search/global-search.component.jsx @@ -1,200 +1,177 @@ -import {useLazyQuery} from "@apollo/client"; -import {AutoComplete, Divider, Input, Space} from "antd"; +import { useLazyQuery } from "@apollo/client"; +import { AutoComplete, Divider, Input, Space } from "antd"; import _ from "lodash"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {Link, useNavigate} from "react-router-dom"; -import {GLOBAL_SEARCH_QUERY} from "../../graphql/search.queries"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import AlertComponent from "../alert/alert.component"; -import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; export default function GlobalSearch() { - const {t} = useTranslation(); - const history = useNavigate(); - const [callSearch, {loading, error, data}] = - useLazyQuery(GLOBAL_SEARCH_QUERY); + const { t } = useTranslation(); + const history = useNavigate(); + const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); - const executeSearch = (v) => { - if ( - v && - v.variables.search && - v.variables.search !== "" && - v.variables.search.length >= 3 - ) - callSearch(v); - }; - const debouncedExecuteSearch = _.debounce(executeSearch, 750); + const executeSearch = (v) => { + if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); + }; + const debouncedExecuteSearch = _.debounce(executeSearch, 750); - const handleSearch = (value) => { - console.log("Handle Search"); - debouncedExecuteSearch({variables: {search: value}}); - }; + const handleSearch = (value) => { + console.log("Handle Search"); + debouncedExecuteSearch({ variables: { search: value } }); + }; - const renderTitle = (title) => { - return {title}; - }; + const renderTitle = (title) => { + return {title}; + }; - const options = data - ? [ - { - label: renderTitle(t("menus.header.search.jobs")), - options: data.search_jobs.map((job) => { - return { - key: job.id, - value: job.ro_number || "N/A", - label: ( - - }> - {job.ro_number || t("general.labels.na")} - {`${job.status || ""}`} - - - - {`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${ - job.v_model_desc || "" - }`} - {`${job.clm_no || ""}`} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.owners")), - options: data.search_owners.map((owner) => { - return { - key: owner.id, - value: OwnerNameDisplayFunction(owner), - label: ( - - } wrap> + const options = data + ? [ + { + label: renderTitle(t("menus.header.search.jobs")), + options: data.search_jobs.map((job) => { + return { + key: job.id, + value: job.ro_number || "N/A", + label: ( + + }> + {job.ro_number || t("general.labels.na")} + {`${job.status || ""}`} - + - - {owner.ownr_ph1} - - - {owner.ownr_ph2} - - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.vehicles")), - options: data.search_vehicles.map((vehicle) => { - return { - key: vehicle.id, - value: `${vehicle.v_model_yr || ""} ${ - vehicle.v_make_desc || "" - } ${vehicle.v_model_desc || ""}`, - label: ( - - }> + {`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`} + {`${job.clm_no || ""}`} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.owners")), + options: data.search_owners.map((owner) => { + return { + key: owner.id, + value: OwnerNameDisplayFunction(owner), + label: ( + + } wrap> - {`${vehicle.v_model_yr || ""} ${ - vehicle.v_make_desc || "" - } ${vehicle.v_model_desc || ""}`} + - {vehicle.plate_no || ""} - - - {vehicle.v_vin || ""} - + {owner.ownr_ph1} + {owner.ownr_ph2} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.vehicles")), + options: data.search_vehicles.map((vehicle) => { + return { + key: vehicle.id, + value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, + label: ( + + }> + + {`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.payments")), - options: data.search_payments.map((payment) => { - return { - key: payment.id, - value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`, - label: ( - - }> - {payment.paymentnum} - {payment.job.ro_number} - {payment.memo || ""} - {payment.amount || ""} - {payment.transactionid || ""} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.bills")), - options: data.search_bills.map((bill) => { - return { - key: bill.id, - value: `${bill.invoice_number} - ${bill.vendor.name}`, - label: ( - - }> - {bill.invoice_number} - {bill.vendor.name} - {bill.date} - - - ), - }; - }), - }, - { - label: renderTitle(t("menus.header.search.phonebook")), - options: data.search_phonebook.map((pb) => { - return { - key: pb.id, - value: `${pb.firstname || ""} ${pb.lastname || ""} ${ - pb.company || "" - }`, - label: ( - - }> - {`${pb.firstname || ""} ${pb.lastname || ""} ${ - pb.company || "" - }`} - {pb.phone1} - {pb.email} - - - ), - }; - }), - }, - ] - : []; + {vehicle.plate_no || ""} + + {vehicle.v_vin || ""} + + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.payments")), + options: data.search_payments.map((payment) => { + return { + key: payment.id, + value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`, + label: ( + + }> + {payment.paymentnum} + {payment.job.ro_number} + {payment.memo || ""} + {payment.amount || ""} + {payment.transactionid || ""} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.bills")), + options: data.search_bills.map((bill) => { + return { + key: bill.id, + value: `${bill.invoice_number} - ${bill.vendor.name}`, + label: ( + + }> + {bill.invoice_number} + {bill.vendor.name} + {bill.date} + + + ) + }; + }) + }, + { + label: renderTitle(t("menus.header.search.phonebook")), + options: data.search_phonebook.map((pb) => { + return { + key: pb.id, + value: `${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`, + label: ( + + }> + {`${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`} + {pb.phone1} + {pb.email} + + + ) + }; + }) + } + ] + : []; - if (error) return ; + if (error) return ; - return ( - { - history(opt.label.props.to); - }} - > - - - ); + return ( + { + history(opt.label.props.to); + }} + > + + + ); } diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 20dae0de5..d0683154e 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -23,50 +23,43 @@ import Icon, { TeamOutlined, ToolFilled, UnorderedListOutlined, - UserOutlined, -} from '@ant-design/icons'; -import { useSplitTreatments } from '@splitsoftware/splitio-react'; -import { Layout, Menu, Switch, Tooltip } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { BsKanban } from 'react-icons/bs'; -import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from 'react-icons/fa'; -import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from 'react-icons/gi'; -import { IoBusinessOutline } from 'react-icons/io5'; -import { RiSurveyLine } from 'react-icons/ri'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { createStructuredSelector } from 'reselect'; -import { - selectRecentItems, - selectSelectedHeader, -} from '../../redux/application/application.selectors'; -import { setModalContext } from '../../redux/modals/modals.actions'; -import { signOutStart } from '../../redux/user/user.actions'; -import { selectBodyshop, selectCurrentUser } from '../../redux/user/user.selectors'; -import { FiLogOut } from 'react-icons/fi'; -import { checkBeta, handleBeta, setBeta } from '../../utils/betaHandler'; -import InstanceRenderManager from '../../utils/instanceRenderMgr'; -import { HasFeatureAccess } from '../feature-wrapper/feature-wrapper.component'; + UserOutlined +} from "@ant-design/icons"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { Layout, Menu, Switch, Tooltip } from "antd"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { BsKanban } from "react-icons/bs"; +import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa"; +import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; +import { IoBusinessOutline } from "react-icons/io5"; +import { RiSurveyLine } from "react-icons/ri"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { signOutStart } from "../../redux/user/user.actions"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { FiLogOut } from "react-icons/fi"; +import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, recentItems: selectRecentItems, selectedHeader: selectSelectedHeader, - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setBillEnterContext: (context) => - dispatch(setModalContext({ context: context, modal: 'billEnter' })), - setTimeTicketContext: (context) => - dispatch(setModalContext({ context: context, modal: 'timeTicket' })), - setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: 'payment' })), - setReportCenterContext: (context) => - dispatch(setModalContext({ context: context, modal: 'reportCenter' })), + setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), + setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })), + setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })), + setReportCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "reportCenter" })), signOutStart: () => dispatch(signOutStart()), - setCardPaymentContext: (context) => - dispatch(setModalContext({ context: context, modal: 'cardPayment' })), + setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })) }); function Header({ @@ -80,14 +73,14 @@ function Header({ setPaymentContext, setReportCenterContext, recentItems, - setCardPaymentContext, + setCardPaymentContext }) { const { - treatments: { ImEXPay, DmsAp, Simple_Inventory }, + treatments: { ImEXPay, DmsAp, Simple_Inventory } } = useSplitTreatments({ attributes: {}, - names: ['ImEXPay', 'DmsAp', 'Simple_Inventory'], - splitKey: bodyshop && bodyshop.imexshopid, + names: ["ImEXPay", "DmsAp", "Simple_Inventory"], + splitKey: bodyshop && bodyshop.imexshopid }); const [betaSwitch, setBetaSwitch] = useState(false); const { t } = useTranslation(); @@ -109,38 +102,38 @@ function Header({ InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'bills', bodyshop }), + promanager: HasFeatureAccess({ featureName: "bills", bodyshop }) }) ) { accountingChildren.push( { - key: 'bills', + key: "bills", icon: , - label: {t('menus.header.bills')}, + label: {t("menus.header.bills")} }, { - key: 'enterbills', + key: "enterbills", icon: , - label: t('menus.header.enterbills'), + label: t("menus.header.enterbills"), onClick: () => { setBillEnterContext({ actions: {}, - context: {}, + context: {} }); - }, + } } ); } - if (Simple_Inventory.treatment === 'on') { + if (Simple_Inventory.treatment === "on") { accountingChildren.push( { - type: 'divider', + type: "divider" }, { - key: 'inventory', + key: "inventory", icon: , - label: {t('menus.header.inventory')}, + label: {t("menus.header.inventory")} } ); } @@ -148,43 +141,43 @@ function Header({ InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'payments', bodyshop }), + promanager: HasFeatureAccess({ featureName: "payments", bodyshop }) }) ) { accountingChildren.push( { - type: 'divider', + type: "divider" }, { - key: 'allpayments', + key: "allpayments", icon: , - label: {t('menus.header.allpayments')}, + label: {t("menus.header.allpayments")} }, { - key: 'enterpayments', + key: "enterpayments", icon: , - label: t('menus.header.enterpayment'), + label: t("menus.header.enterpayment"), onClick: () => { setPaymentContext({ actions: {}, - context: null, + context: null }); - }, + } } ); } - if (ImEXPay.treatment === 'on') { + if (ImEXPay.treatment === "on") { accountingChildren.push({ - key: 'entercardpayments', + key: "entercardpayments", icon: , - label: t('menus.header.entercardpayment'), + label: t("menus.header.entercardpayment"), onClick: () => { setCardPaymentContext({ actions: {}, - context: {}, + context: {} }); - }, + } }); } @@ -192,82 +185,77 @@ function Header({ InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'timetickets', bodyshop }), + promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop }) }) ) { accountingChildren.push( { - type: 'divider', + type: "divider" }, { - key: 'timetickets', + key: "timetickets", icon: , - label: {t('menus.header.timetickets')}, + label: {t("menus.header.timetickets")} } ); if (bodyshop?.md_tasks_presets?.use_approvals) { accountingChildren.push({ - key: 'ttapprovals', + key: "ttapprovals", icon: , - label: {t('menus.header.ttapprovals')}, + label: {t("menus.header.ttapprovals")} }); } accountingChildren.push( { - key: 'entertimetickets', + key: "entertimetickets", icon: , - label: t('menus.header.entertimeticket'), + label: t("menus.header.entertimeticket"), onClick: () => { setTimeTicketContext({ actions: {}, context: { created_by: currentUser.displayName - ? currentUser.email.concat(' | ', currentUser.displayName) - : currentUser.email, - }, + ? currentUser.email.concat(" | ", currentUser.displayName) + : currentUser.email + } }); - }, + } }, { - type: 'divider', + type: "divider" } ); } const accountingExportChildren = [ { - key: 'receivables', - label: ( - {t('menus.header.accounting-receivables')} - ), - }, + key: "receivables", + label: {t("menus.header.accounting-receivables")} + } ]; - if ( - !((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || - DmsAp.treatment === 'on' - ) { + if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") { accountingExportChildren.push({ - key: 'payables', - label: {t('menus.header.accounting-payables')}, + key: "payables", + label: {t("menus.header.accounting-payables")} }); } if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) { accountingExportChildren.push({ - key: 'payments', - label: {t('menus.header.accounting-payments')}, + key: "payments", + label: {t("menus.header.accounting-payments")} }); } accountingExportChildren.push( { - type: 'divider', + type: "divider" }, { - key: 'exportlogs', - label: {t('menus.header.export-logs')}, + key: "exportlogs", + label: {t("menus.header.export-logs")} } ); @@ -275,266 +263,256 @@ function Header({ InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'export', bodyshop }), + promanager: HasFeatureAccess({ featureName: "export", bodyshop }) }) ) { accountingChildren.push({ - key: 'accountingexport', + key: "accountingexport", icon: , - label: t('menus.header.export'), - children: accountingExportChildren, + label: t("menus.header.export"), + children: accountingExportChildren }); } const menuItems = [ { - key: 'home', + key: "home", icon: , - label: {t('menus.header.home')}, + label: {t("menus.header.home")} }, { - key: 'schedule', + key: "schedule", icon: , - label: {t('menus.header.schedule')}, + label: {t("menus.header.schedule")} }, { - key: 'jobssubmenu', - id: 'header-jobs', + key: "jobssubmenu", + id: "header-jobs", icon: , - label: t('menus.header.jobs'), + label: t("menus.header.jobs"), children: [ { - key: 'activejobs', + key: "activejobs", icon: , - label: {t('menus.header.activejobs')}, + label: {t("menus.header.activejobs")} }, { - key: 'readyjobs', + key: "readyjobs", icon: , - label: {t('menus.header.readyjobs')}, + label: {t("menus.header.readyjobs")} }, { - key: 'parts-queue', + key: "parts-queue", icon: , - label: {t('menus.header.parts-queue')}, + label: {t("menus.header.parts-queue")} }, { - key: 'availablejobs', - id: 'header-jobs-available', + key: "availablejobs", + id: "header-jobs-available", icon: , - label: {t('menus.header.availablejobs')}, + label: {t("menus.header.availablejobs")} }, { - key: 'newjob', + key: "newjob", icon: , - label: {t('menus.header.newjob')}, + label: {t("menus.header.newjob")} }, { - type: 'divider', + type: "divider" }, { - key: 'alljobs', + key: "alljobs", icon: , - label: {t('menus.header.alljobs')}, + label: {t("menus.header.alljobs")} }, { - type: 'divider', + type: "divider" }, { - key: 'productionlist', + key: "productionlist", icon: , - label: {t('menus.header.productionlist')}, + label: {t("menus.header.productionlist")} }, ...(InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'visualboard', bodyshop }), + promanager: HasFeatureAccess({ featureName: "visualboard", bodyshop }) }) ? [ { - key: 'productionboard', + key: "productionboard", icon: , - label: ( - {t('menus.header.productionboard')} - ), - }, + label: {t("menus.header.productionboard")} + } ] : []), ...(InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'scoreboard', bodyshop }), + promanager: HasFeatureAccess({ featureName: "scoreboard", bodyshop }) }) ? [ { - type: 'divider', + type: "divider" }, { - key: 'scoreboard', + key: "scoreboard", icon: , - label: {t('menus.header.scoreboard')}, - }, + label: {t("menus.header.scoreboard")} + } ] - : []), - ], + : []) + ] }, { - key: 'customers', + key: "customers", icon: , - label: t('menus.header.customers'), + label: t("menus.header.customers"), children: [ { - key: 'owners', + key: "owners", icon: , - label: {t('menus.header.owners')}, + label: {t("menus.header.owners")} }, { - key: 'vehicles', + key: "vehicles", icon: , - label: {t('menus.header.vehicles')}, - }, - ], + label: {t("menus.header.vehicles")} + } + ] }, ...(InstanceRenderManager({ imex: true, rome: true, - promanager: false, // HasFeatureAccess({ featureName: 'courtesycars', bodyshop }), + promanager: false // HasFeatureAccess({ featureName: 'courtesycars', bodyshop }), }) ? [ { - key: 'ccs', + key: "ccs", icon: , - label: t('menus.header.courtesycars'), + label: t("menus.header.courtesycars"), children: [ { - key: 'courtesycarsall', + key: "courtesycarsall", icon: , - label: {t('menus.header.courtesycars-all')}, + label: {t("menus.header.courtesycars-all")} }, { - key: 'contracts', + key: "contracts", icon: , - label: ( - - {t('menus.header.courtesycars-contracts')} - - ), + label: {t("menus.header.courtesycars-contracts")} }, { - key: 'newcontract', + key: "newcontract", icon: , - label: ( - - {t('menus.header.courtesycars-newcontract')} - - ), - }, - ], - }, + label: {t("menus.header.courtesycars-newcontract")} + } + ] + } ] : []), ...(accountingChildren.length > 0 ? [ { - key: 'accounting', + key: "accounting", icon: , - label: t('menus.header.accounting'), - children: accountingChildren, - }, + label: t("menus.header.accounting"), + children: accountingChildren + } ] : []), { - key: 'phonebook', + key: "phonebook", icon: , - label: {t('menus.header.phonebook')}, + label: {t("menus.header.phonebook")} }, ...(InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'media', bodyshop }), + promanager: HasFeatureAccess({ featureName: "media", bodyshop }) }) ? [ { - key: 'temporarydocs', + key: "temporarydocs", icon: , - label: {t('menus.header.temporarydocs')}, - }, + label: {t("menus.header.temporarydocs")} + } ] : []), { - key: 'shopsubmenu', + key: "shopsubmenu", icon: , - label: t('menus.header.shop'), + label: t("menus.header.shop"), children: [ { - key: 'shop', + key: "shop", icon: , - label: {t('menus.header.shop_config')}, + label: {t("menus.header.shop_config")} }, { - key: 'dashboard', + key: "dashboard", icon: , - label: {t('menus.header.dashboard')}, + label: {t("menus.header.dashboard")} }, { - key: 'reportcenter', + key: "reportcenter", icon: , - label: t('menus.header.reportcenter'), + label: t("menus.header.reportcenter"), onClick: () => { setReportCenterContext({ actions: {}, - context: {}, + context: {} }); - }, + } }, { - key: 'shop-vendors', + key: "shop-vendors", icon: , - label: {t('menus.header.shop_vendors')}, + label: {t("menus.header.shop_vendors")} }, ...(InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'csi', bodyshop }), + promanager: HasFeatureAccess({ featureName: "csi", bodyshop }) }) ? [ { - key: 'shop-csi', + key: "shop-csi", icon: , - label: {t('menus.header.shop_csi')}, - }, + label: {t("menus.header.shop_csi")} + } ] - : []), - ], + : []) + ] }, { - key: 'user', - label: currentUser.displayName || currentUser.email || t('general.labels.unknown'), + key: "user", + label: currentUser.displayName || currentUser.email || t("general.labels.unknown"), children: [ { - key: 'signout', + key: "signout", icon: , danger: true, - label: t('user.actions.signout'), - onClick: () => signOutStart(), + label: t("user.actions.signout"), + onClick: () => signOutStart() }, { - key: 'help', + key: "help", icon: , - label: t('menus.header.help'), + label: t("menus.header.help"), onClick: () => { window.open( InstanceRenderManager({ - imex: 'https://help.imex.online/', - rome: 'https://rometech.com//', - promanager: 'https://web-est.com', + imex: "https://help.imex.online/", + rome: "https://rometech.com//", + promanager: "https://web-est.com" }), - '_blank' + "_blank" ); - }, + } }, // { // key: 'rescue', @@ -548,21 +526,21 @@ function Header({ ...(InstanceRenderManager({ imex: true, rome: true, - promanager: HasFeatureAccess({ featureName: 'timetickets', bodyshop }), + promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop }) }) ? [ { - key: 'shiftclock', + key: "shiftclock", icon: , - label: {t('menus.header.shiftclock')}, - }, + label: {t("menus.header.shiftclock")} + } ] : []), { - key: 'profile', + key: "profile", icon: , - label: {t('menus.currentuser.profile')}, - }, + label: {t("menus.currentuser.profile")} + } // { // key: 'langselecter', // label: t("menus.currentuser.languageselector"), @@ -590,41 +568,47 @@ function Header({ // }, // ] // }, - ], + ] }, { - key: 'recent', + key: "recent", icon: , children: recentItems.map((i, idx) => ({ key: idx, - label: {i.label}, - })), - }, + label: {i.label} + })) + } ]; - menuItems.push({ - key: 'beta-switch', - style: { marginLeft: 'auto' }, - label: ( - - - Try the new app - - - ), + InstanceRenderManager({ + executeFunction: true, + args: [], + imex: () => { + menuItems.push({ + key: "beta-switch", + style: { marginLeft: "auto" }, + label: ( + + + Try the new app + + + ) + }); + } }); return ( ({ - setUserLanguage: (language) => dispatch(setUserLanguage(language)), + setUserLanguage: (language) => dispatch(setUserLanguage(language)) }); -export function HeaderContainer({setUserLanguage}) { - const handleMenuClick = (e) => { - if (e.item.props.actiontype === "lang-select") { - i18next.changeLanguage(e.key, (err, t) => { - if (err) { - logImEXEvent("language_change_error", {error: err}); +export function HeaderContainer({ setUserLanguage }) { + const handleMenuClick = (e) => { + if (e.item.props.actiontype === "lang-select") { + i18next.changeLanguage(e.key, (err, t) => { + if (err) { + logImEXEvent("language_change_error", { error: err }); - return console.log("Error encountered when changing languages.", err); - } - logImEXEvent("language_change", {language: e.key}); - - setUserLanguage(e.key); - }); + return console.log("Error encountered when changing languages.", err); } - }; + logImEXEvent("language_change", { language: e.key }); - return ; + setUserLanguage(e.key); + }); + } + }; + + return ; } export default connect(null, mapDispatchToProps)(HeaderContainer); diff --git a/client/src/components/help-rescue/help-rescue.component.jsx b/client/src/components/help-rescue/help-rescue.component.jsx index b9a8ac9bc..07f910de3 100644 --- a/client/src/components/help-rescue/help-rescue.component.jsx +++ b/client/src/components/help-rescue/help-rescue.component.jsx @@ -1,53 +1,53 @@ -import {Button, Input, Space} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import { Button, Input, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; export default function HelpRescue() { - const {t} = useTranslation(); - const [code, setCode] = useState(""); + const { t } = useTranslation(); + const [code, setCode] = useState(""); - const handleClick = async () => { - var bodyFormData = new FormData(); - bodyFormData.append("Code", code); - bodyFormData.append("hostederrorhandling", 1); - await fetch("https://secure.logmeinrescue.com/Customer/Code.aspx", { - mode: "no-cors", - method: "POST", - body: bodyFormData, - }); - }; + const handleClick = async () => { + var bodyFormData = new FormData(); + bodyFormData.append("Code", code); + bodyFormData.append("hostederrorhandling", 1); + await fetch("https://secure.logmeinrescue.com/Customer/Code.aspx", { + mode: "no-cors", + method: "POST", + body: bodyFormData + }); + }; - return ( - - - {t("help.labels.rescuedesc")} - setCode(e.target.value)} - value={code} - placeholder={t("help.labels.codeplacholder")} - /> - {t("help.actions.connect")} + return ( + + + {t("help.labels.rescuedesc")} + setCode(e.target.value)} + value={code} + placeholder={t("help.labels.codeplacholder")} + /> + {t("help.actions.connect")} - { - alert(); - }} - > - Enter your 6-digit code: - - - - - - - - - - ); + { + alert(); + }} + > + Enter your 6-digit code: + + + + + + + + + + ); } diff --git a/client/src/components/indefinite-loading/indefinite-loading.component.jsx b/client/src/components/indefinite-loading/indefinite-loading.component.jsx index d38edeaa9..37e87344b 100644 --- a/client/src/components/indefinite-loading/indefinite-loading.component.jsx +++ b/client/src/components/indefinite-loading/indefinite-loading.component.jsx @@ -1,43 +1,45 @@ -import {useNProgress} from "@tanem/react-nprogress"; +import { useNProgress } from "@tanem/react-nprogress"; import React from "react"; -export default function IndefiniteLoading({loading}) { - const {animationDuration, isFinished, progress} = useNProgress({ - isAnimating: loading, - }); +export default function IndefiniteLoading({ loading }) { + const { animationDuration, isFinished, progress } = useNProgress({ + isAnimating: loading + }); - return ( + return ( + + - - - - - ); + style={{ + boxShadow: "0 0 10px #29d, 0 0 5px #29d", + display: "block", + height: "100%", + opacity: 1, + position: "absolute", + right: 0, + transform: "rotate(3deg) translate(0px, -4px)", + width: 100 + }} + /> + + + ); } diff --git a/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx b/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx index fca8815d1..9972478f5 100644 --- a/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx +++ b/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx @@ -1,67 +1,62 @@ -import {Button} from "antd"; +import { Button } from "antd"; import React from "react"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import dayjs from "../../utils/day"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setBillEnterContext: (context) => - dispatch(setModalContext({context: context, modal: "billEnter"})), + setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })) }); export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo); -export function InventoryBillRo({ - bodyshop, - setBillEnterContext, - inventoryline, - }) { - const {t} = useTranslation(); - return ( - { - setBillEnterContext({ - actions: { - //refetch: refetch - }, - context: { - disableInvNumber: true, - //job: { id: job.id }, - consumeinventoryid: inventoryline.id, - bill: { - vendorid: bodyshop.inhousevendorid, - invoice_number: "ih", - isinhouse: true, - date: dayjs(), - total: 0, - billlines: [{}], - // billlines: selectedLines.map((p) => { - // return { - // joblineid: p.id, - // actual_price: p.act_price, - // actual_cost: 0, //p.act_price, - // line_desc: p.line_desc, - // line_remarks: p.line_remarks, - // part_type: p.part_type, - // quantity: p.quantity || 1, - // applicable_taxes: { - // local: false, - // state: false, - // federal: false, - // }, - // }; - // }), - }, - }, - }); - }} - > - {t("inventory.actions.addtoro")} - - ); +export function InventoryBillRo({ bodyshop, setBillEnterContext, inventoryline }) { + const { t } = useTranslation(); + return ( + { + setBillEnterContext({ + actions: { + //refetch: refetch + }, + context: { + disableInvNumber: true, + //job: { id: job.id }, + consumeinventoryid: inventoryline.id, + bill: { + vendorid: bodyshop.inhousevendorid, + invoice_number: "ih", + isinhouse: true, + date: dayjs(), + total: 0, + billlines: [{}] + // billlines: selectedLines.map((p) => { + // return { + // joblineid: p.id, + // actual_price: p.act_price, + // actual_cost: 0, //p.act_price, + // line_desc: p.line_desc, + // line_remarks: p.line_remarks, + // part_type: p.part_type, + // quantity: p.quantity || 1, + // applicable_taxes: { + // local: false, + // state: false, + // federal: false, + // }, + // }; + // }), + } + } + }); + }} + > + {t("inventory.actions.addtoro")} + + ); } diff --git a/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx b/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx index ec7e8c20f..5175bac47 100644 --- a/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx +++ b/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx @@ -1,67 +1,60 @@ -import {DeleteFilled} from "@ant-design/icons"; -import {useMutation} from "@apollo/client"; -import {Button, notification, Popconfirm} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {DELETE_INVENTORY_LINE} from "../../graphql/inventory.queries"; +import { DeleteFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, notification, Popconfirm } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; -export default function InventoryLineDelete({ - inventoryline, - disabled, - refetch, - }) { - const [loading, setLoading] = useState(false); - const {t} = useTranslation(); - const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE); +export default function InventoryLineDelete({ inventoryline, disabled, refetch }) { + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE); - const handleDelete = async () => { - setLoading(true); - const result = await deleteInventoryLine({ - variables: {lineId: inventoryline.id}, - // update(cache, { errors }) { - // cache.modify({ - // fields: { - // inventory(existingInventory, { readField }) { - // console.log(existingInventory); - // return existingInventory.filter( - // (invRef) => inventoryline.id !== readField("id", invRef) - // ); - // }, - // }, - // }); - // }, - }); + const handleDelete = async () => { + setLoading(true); + const result = await deleteInventoryLine({ + variables: { lineId: inventoryline.id } + // update(cache, { errors }) { + // cache.modify({ + // fields: { + // inventory(existingInventory, { readField }) { + // console.log(existingInventory); + // return existingInventory.filter( + // (invRef) => inventoryline.id !== readField("id", invRef) + // ); + // }, + // }, + // }); + // }, + }); - if (!!!result.errors) { - notification["success"]({message: t("inventory.successes.deleted")}); - } else { - //Check if it's an fkey violation. + if (!!!result.errors) { + notification["success"]({ message: t("inventory.successes.deleted") }); + } else { + //Check if it's an fkey violation. - notification["error"]({ - message: t("bills.errors.deleting", { - error: JSON.stringify(result.errors), - }), - }); - } - if (refetch) refetch(); - setLoading(false); - }; + notification["error"]({ + message: t("bills.errors.deleting", { + error: JSON.stringify(result.errors) + }) + }); + } + if (refetch) refetch(); + setLoading(false); + }; - return ( - >}> - - - - - - - ); + return ( + >}> + + + + + + + ); } diff --git a/client/src/components/inventory-list/inventory-list.component.jsx b/client/src/components/inventory-list/inventory-list.component.jsx index 96a5e64de..de8487add 100644 --- a/client/src/components/inventory-list/inventory-list.component.jsx +++ b/client/src/components/inventory-list/inventory-list.component.jsx @@ -1,224 +1,206 @@ -import {EditFilled, FileAddFilled, SyncOutlined} from "@ant-design/icons"; -import {Button, Card, Input, Space, Table, Typography} from "antd"; +import { EditFilled, FileAddFilled, SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Input, Space, Table, Typography } from "antd"; import queryString from "query-string"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {Link, useLocation, useNavigate} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component"; import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser - bodyshop: selectBodyshop, + //currentUser: selectCurrentUser + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setInventoryUpsertContext: (context) => - dispatch(setModalContext({context: context, modal: "inventoryUpsert"})), + setInventoryUpsertContext: (context) => dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })) }); -export function JobsList({bodyshop, refetch, loading, jobs, total, setInventoryUpsertContext,}) { - const search = queryString.parse(useLocation().search); - const {page, sortcolumn, sortorder} = search; - const history = useNavigate(); +export function JobsList({ bodyshop, refetch, loading, jobs, total, setInventoryUpsertContext }) { + const search = queryString.parse(useLocation().search); + const { page, sortcolumn, sortorder } = search; + const history = useNavigate(); - const {t} = useTranslation(); - const columns = [ - { - title: t("billlines.fields.line_desc"), - dataIndex: "line_desc", - key: "line_desc", + const { t } = useTranslation(); + const columns = [ + { + title: t("billlines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", - sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc), - sortOrder: sortcolumn === "line_desc" && sortorder, - render: (text, record) => - record.billline?.bill?.job ? ( - - {text} - {`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`} - - ) : ( - text - ), - }, - { - title: t("inventory.labels.frombillinvoicenumber"), - dataIndex: "vendorname", - key: "vendorname", - ellipsis: true, - //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc), + sortOrder: sortcolumn === "line_desc" && sortorder, + render: (text, record) => + record.billline?.bill?.job ? ( + + {text} + {`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`} + + ) : ( + text + ) + }, + { + title: t("inventory.labels.frombillinvoicenumber"), + dataIndex: "vendorname", + key: "vendorname", + ellipsis: true, + //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - //sortOrder: sortcolumn === "ownr_ln" && sortorder, - render: (text, record) => - ( - (record.billline?.bill?.invoice_number || "") + - " " + - (record.manualinvoicenumber || "") - ).trim(), - }, - { - title: t("inventory.labels.fromvendor"), - dataIndex: "vendorname", - key: "vendorname", - ellipsis: true, - //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + //sortOrder: sortcolumn === "ownr_ln" && sortorder, + render: (text, record) => + ((record.billline?.bill?.invoice_number || "") + " " + (record.manualinvoicenumber || "")).trim() + }, + { + title: t("inventory.labels.fromvendor"), + dataIndex: "vendorname", + key: "vendorname", + ellipsis: true, + //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - //sortOrder: sortcolumn === "ownr_ln" && sortorder, - render: (text, record) => - ( - (record.billline?.bill?.vendor?.name || "") + - " " + - (record.manualvendor || "") - ).trim(), - }, - { - title: t("billlines.fields.actual_price"), - dataIndex: "actual_price", - key: "actual_price", + //sortOrder: sortcolumn === "ownr_ln" && sortorder, + render: (text, record) => ((record.billline?.bill?.vendor?.name || "") + " " + (record.manualvendor || "")).trim() + }, + { + title: t("billlines.fields.actual_price"), + dataIndex: "actual_price", + key: "actual_price", - render: (text, record) => ( - {record.actual_price} - ), - }, - { - title: t("billlines.fields.actual_cost"), - dataIndex: "actual_cost", - key: "actual_cost", + render: (text, record) => {record.actual_price} + }, + { + title: t("billlines.fields.actual_cost"), + dataIndex: "actual_cost", + key: "actual_cost", - render: (text, record) => ( - {record.actual_cost} - ), - }, - { - title: t("inventory.fields.comment"), - dataIndex: "comment", - key: "comment", - }, - { - title: t("inventory.labels.consumedbyjob"), - dataIndex: "consumedbyjob", - key: "consumedbyjob", + render: (text, record) => {record.actual_cost} + }, + { + title: t("inventory.fields.comment"), + dataIndex: "comment", + key: "comment" + }, + { + title: t("inventory.labels.consumedbyjob"), + dataIndex: "consumedbyjob", + key: "consumedbyjob", - ellipsis: true, - render: (text, record) => - record.bill?.job?.ro_number ? ( - - {record.bill?.job?.ro_number} - - ) : ( - - ), - }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", + ellipsis: true, + render: (text, record) => + record.bill?.job?.ro_number ? ( + {record.bill?.job?.ro_number} + ) : ( + + ) + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", - ellipsis: true, - render: (text, record) => ( - - { - setInventoryUpsertContext({ - actions: {refetch: refetch}, - context: { - existingInventory: record, - }, - }); - }} - > - - - - - ), - }, - ]; + ellipsis: true, + render: (text, record) => ( + + { + setInventoryUpsertContext({ + actions: { refetch: refetch }, + context: { + existingInventory: record + } + }); + }} + > + + + + + ) + } + ]; - const handleTableChange = (pagination, filters, sorter) => { - search.page = pagination.current; - search.sortcolumn = sorter.column && sorter.column.key; - search.sortorder = sorter.order; - history({search: queryString.stringify(search)}); - }; + const handleTableChange = (pagination, filters, sorter) => { + search.page = pagination.current; + search.sortcolumn = sorter.column && sorter.column.key; + search.sortorder = sorter.order; + history({ search: queryString.stringify(search) }); + }; - return ( - - {search.search && ( - <> - - {t("general.labels.searchresults", {search: search.search})} - - { - delete search.search; - history({search: queryString.stringify(search)}); - }} - > - {t("general.actions.clear")} - - > - )} - { - setInventoryUpsertContext({ - actions: {refetch: refetch}, - context: {}, - }); - }} - > - - - { - if (search.showall) delete search.showall; - else { - search.showall = true; - } - history({search: queryString.stringify(search)}); - }} - > - {search.showall - ? t("inventory.labels.showavailable") - : t("inventory.labels.showall")} - - - refetch()}> - - - { - search.search = value; - history({search: queryString.stringify(search)}); - }} - enterButton - /> - - } - > - + {search.search && ( + <> + + {t("general.labels.searchresults", { search: search.search })} + + { + delete search.search; + history({ search: queryString.stringify(search) }); }} - columns={columns} - rowKey="id" - dataSource={jobs} - onChange={handleTableChange} - /> - - ); + > + {t("general.actions.clear")} + + > + )} + { + setInventoryUpsertContext({ + actions: { refetch: refetch }, + context: {} + }); + }} + > + + + { + if (search.showall) delete search.showall; + else { + search.showall = true; + } + history({ search: queryString.stringify(search) }); + }} + > + {search.showall ? t("inventory.labels.showavailable") : t("inventory.labels.showall")} + + + refetch()}> + + + { + search.search = value; + history({ search: queryString.stringify(search) }); + }} + enterButton + /> + + } + > + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(JobsList); diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx index 3990cdfce..6d154cd20 100644 --- a/client/src/components/inventory-list/inventory-list.container.jsx +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -1,62 +1,55 @@ -import {useQuery} from "@apollo/client"; +import { useQuery } from "@apollo/client"; import queryString from "query-string"; import React from "react"; -import {connect} from "react-redux"; -import {useLocation} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {QUERY_INVENTORY_PAGINATED} from "../../graphql/inventory.queries"; -import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions"; +import { connect } from "react-redux"; +import { useLocation } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries"; +import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import AlertComponent from "../alert/alert.component"; import InventoryListPaginated from "./inventory-list.component"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ - //bodyshop: selectBodyshop, + //bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ - setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), - setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), + setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) }); -export function InventoryList({setBreadcrumbs, setSelectedHeader}) { - const searchParams = queryString.parse(useLocation().search); - const {page, sortcolumn, sortorder, search, showall} = searchParams; +export function InventoryList({ setBreadcrumbs, setSelectedHeader }) { + const searchParams = queryString.parse(useLocation().search); + const { page, sortcolumn, sortorder, search, showall } = searchParams; - const {loading, error, data, refetch} = useQuery( - QUERY_INVENTORY_PAGINATED, + const { loading, error, data, refetch } = useQuery(QUERY_INVENTORY_PAGINATED, { + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + variables: { + search: search || "", + offset: page ? (page - 1) * pageLimit : 0, + limit: pageLimit, + consumedIsNull: showall === "true" ? null : true, + order: [ { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - variables: { - search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, - consumedIsNull: showall === "true" ? null : true, - order: [ - { - [sortcolumn || "created_at"]: - sortorder && sortorder !== "false" - ? sortorder === "descend" - ? "desc" - : "asc" - : "desc", - }, - ], - }, + [sortcolumn || "created_at"]: + sortorder && sortorder !== "false" ? (sortorder === "descend" ? "desc" : "asc") : "desc" } - ); + ] + } + }); - if (error) return ; - return ( - - ); + if (error) return ; + return ( + + ); } export default connect(mapStateToProps, mapDispatchToProps)(InventoryList); diff --git a/client/src/components/inventory-upsert-modal/inventory-upsert-modal.component.jsx b/client/src/components/inventory-upsert-modal/inventory-upsert-modal.component.jsx index dcf7fb4a7..e718baef6 100644 --- a/client/src/components/inventory-upsert-modal/inventory-upsert-modal.component.jsx +++ b/client/src/components/inventory-upsert-modal/inventory-upsert-modal.component.jsx @@ -1,69 +1,48 @@ -import {Form, Input, Space} from "antd"; +import { Form, Input, Space } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectInventoryUpsert} from "../../redux/modals/modals.selectors"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectInventoryUpsert } from "../../redux/modals/modals.selectors"; import FormItemCurrency from "../form-items-formatted/currency-form-item.component"; const mapStateToProps = createStructuredSelector({ - inventoryUpsertModal: selectInventoryUpsert, + inventoryUpsertModal: selectInventoryUpsert }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(NoteUpsertModalComponent); +export default connect(mapStateToProps, mapDispatchToProps)(NoteUpsertModalComponent); -export function NoteUpsertModalComponent({form, inventoryUpsertModal}) { - const {t} = useTranslation(); - const {existingInventory} = inventoryUpsertModal.context; +export function NoteUpsertModalComponent({ form, inventoryUpsertModal }) { + const { t } = useTranslation(); + const { existingInventory } = inventoryUpsertModal.context; - return ( - - - - - - - + return ( + + + + + + + - {!existingInventory && ( - <> - - - - - - - - - - - - - > - )} - - ); + {!existingInventory && ( + <> + + + + + + + + + + + + + > + )} + + ); } diff --git a/client/src/components/inventory-upsert-modal/inventory-upsert-modal.container.jsx b/client/src/components/inventory-upsert-modal/inventory-upsert-modal.container.jsx index 819aca650..a7fa56646 100644 --- a/client/src/components/inventory-upsert-modal/inventory-upsert-modal.container.jsx +++ b/client/src/components/inventory-upsert-modal/inventory-upsert-modal.container.jsx @@ -1,120 +1,108 @@ -import {useMutation} from "@apollo/client"; -import {Form, Modal, notification} from "antd"; -import React, {useEffect} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {INSERT_INVENTORY_LINE, UPDATE_INVENTORY_LINE,} from "../../graphql/inventory.queries"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectInventoryUpsert} from "../../redux/modals/modals.selectors"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; +import { useMutation } from "@apollo/client"; +import { Form, Modal, notification } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { INSERT_INVENTORY_LINE, UPDATE_INVENTORY_LINE } from "../../graphql/inventory.queries"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectInventoryUpsert } from "../../redux/modals/modals.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import InventoryUpsertModal from "./inventory-upsert-modal.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, - inventoryUpsertModal: selectInventoryUpsert, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + inventoryUpsertModal: selectInventoryUpsert }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")), + toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")) }); -export function InventoryUpsertModalContainer({ - currentUser, - bodyshop, - inventoryUpsertModal, - toggleModalVisible, - }) { - const {t} = useTranslation(); - const [insertInventory] = useMutation(INSERT_INVENTORY_LINE); - const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE); +export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventoryUpsertModal, toggleModalVisible }) { + const { t } = useTranslation(); + const [insertInventory] = useMutation(INSERT_INVENTORY_LINE); + const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE); - const {open, context, actions} = inventoryUpsertModal; - const {existingInventory} = context; - const {refetch} = actions; + const { open, context, actions } = inventoryUpsertModal; + const { existingInventory } = context; + const { refetch } = actions; - const [form] = Form.useForm(); + const [form] = Form.useForm(); - useEffect(() => { - //Required to prevent infinite looping. - if (existingInventory && open) { - form.setFieldsValue(existingInventory); - } else if (!existingInventory && open) { - form.resetFields(); + useEffect(() => { + //Required to prevent infinite looping. + if (existingInventory && open) { + form.setFieldsValue(existingInventory); + } else if (!existingInventory && open) { + form.resetFields(); + } + }, [existingInventory, form, open]); + + const handleFinish = async (formValues) => { + const values = formValues; + + if (existingInventory) { + logImEXEvent("inventory_update"); + + updateInventoryLine({ + variables: { + inventoryId: existingInventory.id, + inventoryItem: values } - }, [existingInventory, form, open]); + }).then((r) => { + notification["success"]({ + message: t("inventory.successes.updated") + }); + }); + // if (refetch) refetch(); + toggleModalVisible(); + } else { + logImEXEvent("inventory_insert"); - const handleFinish = async (formValues) => { - const values = formValues; - - if (existingInventory) { - logImEXEvent("inventory_update"); - - updateInventoryLine({ - variables: { - inventoryId: existingInventory.id, - inventoryItem: values, - }, - }).then((r) => { - notification["success"]({ - message: t("inventory.successes.updated"), - }); - }); - // if (refetch) refetch(); - toggleModalVisible(); - } else { - logImEXEvent("inventory_insert"); - - await insertInventory({ - variables: { - inventoryItem: {shopid: bodyshop.id, ...values}, - }, - update(cache, {data}) { - cache.modify({ - fields: { - inventory(existingInv) { - return [...existingInv, data.insert_inventory_one]; - }, - }, - }); - }, - }); - - if (refetch) refetch(); - form.resetFields(); - toggleModalVisible(); - notification["success"]({ - message: t("inventory.successes.inserted"), - }); - } - }; - - return ( - { - form.submit(); - }} - onCancel={() => { - toggleModalVisible(); - }} - destroyOnClose - > - - - - - ); + }); + } + }); + + if (refetch) refetch(); + form.resetFields(); + toggleModalVisible(); + notification["success"]({ + message: t("inventory.successes.inserted") + }); + } + }; + + return ( + { + form.submit(); + }} + onCancel={() => { + toggleModalVisible(); + }} + destroyOnClose + > + + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(InventoryUpsertModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(InventoryUpsertModalContainer); diff --git a/client/src/components/job-3rd-party-modal/job-3rd-party-modal.component.jsx b/client/src/components/job-3rd-party-modal/job-3rd-party-modal.component.jsx index 2ae6bcdac..b0f5cac79 100644 --- a/client/src/components/job-3rd-party-modal/job-3rd-party-modal.component.jsx +++ b/client/src/components/job-3rd-party-modal/job-3rd-party-modal.component.jsx @@ -1,227 +1,176 @@ -import {useQuery} from "@apollo/client"; -import {Button, Form, Input, InputNumber, Modal, Radio, Select} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR} from "../../graphql/vendors.queries"; -import {selectTechnician} from "../../redux/tech/tech.selectors"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { useQuery } from "@apollo/client"; +import { Button, Form, Input, InputNumber, Modal, Radio, Select } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - technician: selectTechnician, + bodyshop: selectBodyshop, + technician: selectTechnician }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal); -export function Jobd3RdPartyModal({bodyshop, jobId, job, technician}) { - const [isModalVisible, setIsModalVisible] = useState(false); - const {t} = useTranslation(); - const [form] = Form.useForm(); - const {data: VendorAutoCompleteData} = useQuery( - SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR, - { - skip: !isModalVisible, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - } +export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) { + const [isModalVisible, setIsModalVisible] = useState(false); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR, { + skip: !isModalVisible, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + + const showModal = () => { + form.setFieldsValue({ + ded_amt: job.ded_amt, + depreciation: job.depreciation_taxes, + custgst: job.ca_customer_gst + }); + setIsModalVisible(true); + }; + + const handleOk = () => { + form.submit(); + setIsModalVisible(false); + }; + + const handleCancel = () => { + form.resetFields(); + setIsModalVisible(false); + }; + const handleFinish = (values) => { + const { sendtype, ...restVals } = values; + + GenerateDocument( + { + name: TemplateList("job_special").special_thirdpartypayer.key, + variables: { id: jobId }, + context: restVals + }, + { subject: TemplateList("job_special").special_thirdpartypayer.subject }, + sendtype ); + }; - const showModal = () => { - form.setFieldsValue({ - ded_amt: job.ded_amt, - depreciation: job.depreciation_taxes, - custgst: job.ca_customer_gst, - }); - setIsModalVisible(true); - }; + const handleInsSelect = (value, option) => { + form.setFieldsValue({ + addr1: option.obj.name, + addr2: option.obj.street1, + addr3: option.obj.street2, + city: option.obj.city, + state: option.obj.state, + zip: option.obj.zip, + vendorid: null + }); + }; - const handleOk = () => { - form.submit(); - setIsModalVisible(false); - }; + const handleVendorSelect = (vendorid, opt) => { + const vendor = VendorAutoCompleteData.vendors.filter((v) => v.id === vendorid)[0]; + if (vendor) { + form.setFieldsValue({ + addr1: vendor.name, + addr2: vendor.street1, + addr3: vendor.street2, + city: vendor.city, + state: vendor.state, + zip: vendor.zip, + ins_co_id: null + }); + } + }; - const handleCancel = () => { - form.resetFields(); - setIsModalVisible(false); - }; - const handleFinish = (values) => { - const {sendtype, ...restVals} = values; - - GenerateDocument( - { - name: TemplateList("job_special").special_thirdpartypayer.key, - variables: {id: jobId}, - context: restVals, - }, - {subject: TemplateList("job_special").special_thirdpartypayer.subject}, - sendtype - ); - }; - - const handleInsSelect = (value, option) => { - form.setFieldsValue({ - addr1: option.obj.name, - addr2: option.obj.street1, - addr3: option.obj.street2, - city: option.obj.city, - state: option.obj.state, - zip: option.obj.zip, - vendorid: null, - }); - }; - - const handleVendorSelect = (vendorid, opt) => { - const vendor = VendorAutoCompleteData.vendors.filter( - (v) => v.id === vendorid - )[0]; - if (vendor) { - form.setFieldsValue({ - addr1: vendor.name, - addr2: vendor.street1, - addr3: vendor.street2, - city: vendor.city, - state: vendor.state, - zip: vendor.zip, - ins_co_id: null, - }); - } - }; - - return ( - <> - {t("printcenter.jobs.3rdpartypayer")} - - - - - - - - {bodyshop.md_ins_cos.map((s) => ( - - {s.name} - - ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {!technician ? ( - {t("parts_orders.labels.email")} - ) : null} - {t("parts_orders.labels.print")} - - - - - > - ); + return ( + <> + {t("printcenter.jobs.3rdpartypayer")} + + + + + + + + {bodyshop.md_ins_cos.map((s) => ( + + {s.name} + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {!technician ? {t("parts_orders.labels.email")} : null} + {t("parts_orders.labels.print")} + + + + + > + ); } diff --git a/client/src/components/job-at-change/job-at-change.component.jsx b/client/src/components/job-at-change/job-at-change.component.jsx index 26f581d10..07a652a15 100644 --- a/client/src/components/job-at-change/job-at-change.component.jsx +++ b/client/src/components/job-at-change/job-at-change.component.jsx @@ -1,65 +1,62 @@ import React from "react"; -import {useMutation} from "@apollo/client"; -import {UPDATE_JOB} from "../../graphql/jobs.queries"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {Dropdown, notification} from "antd"; -import {DownOutlined} from "@ant-design/icons"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useMutation } from "@apollo/client"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { Dropdown, notification } from "antd"; +import { DownOutlined } from "@ant-design/icons"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function JobAltTransportChange({bodyshop, job}) { - const [updateJob] = useMutation(UPDATE_JOB); - const {t} = useTranslation(); +export function JobAltTransportChange({ bodyshop, job }) { + const [updateJob] = useMutation(UPDATE_JOB); + const { t } = useTranslation(); - const onClick = async ({key}) => { - const result = await updateJob({ - variables: { - jobId: job.id, - job: {alt_transport: key === "null" ? null : key}, - }, - }); + const onClick = async ({ key }) => { + const result = await updateJob({ + variables: { + jobId: job.id, + job: { alt_transport: key === "null" ? null : key } + } + }); - if (!!!result.errors) { - // notification["success"]({ message: t("appointments.successes.saved") }); - } else { - notification["error"]({ - message: t("jobs.errors.saving", { - error: JSON.stringify(result.errors), - }), - }); - } - }; + if (!!!result.errors) { + // notification["success"]({ message: t("appointments.successes.saved") }); + } else { + notification["error"]({ + message: t("jobs.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } + }; - const menu = { - items: [ - ...(bodyshop.appt_alt_transport || []).map((alt) => ({ - key: alt, - label: alt, - })), - {key: "null", label: t("general.actions.clear")}, - ], - onClick: onClick, - defaultSelectedKeys: [job && job.alt_transport] - }; + const menu = { + items: [ + ...(bodyshop.appt_alt_transport || []).map((alt) => ({ + key: alt, + label: alt + })), + { key: "null", label: t("general.actions.clear") } + ], + onClick: onClick, + defaultSelectedKeys: [job && job.alt_transport] + }; - return ( - - e.preventDefault()}> - - - - ); + return ( + + e.preventDefault()}> + + + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(JobAltTransportChange); +export default connect(mapStateToProps, mapDispatchToProps)(JobAltTransportChange); diff --git a/client/src/components/job-at-change/schedule-event.color.component.jsx b/client/src/components/job-at-change/schedule-event.color.component.jsx index 5de77c758..e66a8b6d9 100644 --- a/client/src/components/job-at-change/schedule-event.color.component.jsx +++ b/client/src/components/job-at-change/schedule-event.color.component.jsx @@ -1,72 +1,70 @@ import React from "react"; -import {useMutation} from "@apollo/client"; -import {UPDATE_APPOINTMENT} from "../../graphql/appointments.queries"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {Dropdown, notification} from "antd"; -import {DownOutlined} from "@ant-design/icons"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { useMutation } from "@apollo/client"; +import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { Dropdown, notification } from "antd"; +import { DownOutlined } from "@ant-design/icons"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ScheduleEventColor({bodyshop, event}) { - const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); - const {t} = useTranslation(); +export function ScheduleEventColor({ bodyshop, event }) { + const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); + const { t } = useTranslation(); - const onClick = async ({key}) => { - const result = await updateAppointment({ - variables: { - appid: event.id, - app: {color: key === "null" ? null : key}, - }, - }); + const onClick = async ({ key }) => { + const result = await updateAppointment({ + variables: { + appid: event.id, + app: { color: key === "null" ? null : key } + } + }); - if (!!!result.errors) { - notification["success"]({message: t("appointments.successes.saved")}); - } else { - notification["error"]({ - message: t("appointments.errors.saving", { - error: JSON.stringify(result.errors), - }), - }); - } - }; + if (!!!result.errors) { + notification["success"]({ message: t("appointments.successes.saved") }); + } else { + notification["error"]({ + message: t("appointments.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } + }; - const selectedColor = - event.color && - bodyshop.appt_colors && - bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0] - ?.label; + const selectedColor = + event.color && + bodyshop.appt_colors && + bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]?.label; + const menu = { + defaultSelectedKeys: [event.color], + onClick: onClick, + items: [ + ...(bodyshop.appt_colors || []).map((color) => ({ + key: color.color.hex, + label: color.label, + style: { color: color.color.hex } + })), + { type: "divider" }, + { key: "null", label: t("general.actions.clear") } + ] + }; - const menu = { - defaultSelectedKeys: [event.color], - onClick: onClick, - items: [ - ...(bodyshop.appt_colors || []).map((color) => ({ - key: color.color.hex, - label: color.label, - style: {color: color.color.hex}, - })), - {type: "divider"}, - {key: "null", label: t("general.actions.clear")}, - ] - }; - - return ( - - e.preventDefault()}> - {selectedColor} - - - - ); + return ( + + e.preventDefault()}> + {selectedColor} + + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventColor); diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index d09938940..b34e90668 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -1,19 +1,19 @@ -import {AlertFilled} from "@ant-design/icons"; -import {Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space,} from "antd"; +import { AlertFilled } from "@ant-design/icons"; +import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space } from "antd"; import parsePhoneNumber from "libphonenumber-js"; import dayjs from "../../utils/day"; import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {Link, useLocation, useNavigate} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {openChatByPhone, setMessage,} from "../../redux/messaging/messaging.actions"; -import {setModalContext} from "../../redux/modals/modals.actions"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import {GenerateDocument} from "../../utils/RenderTemplate"; -import {TemplateList} from "../../utils/TemplateConstants"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import DataLabel from "../data-label/data-label.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; @@ -21,358 +21,330 @@ import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event. import ScheduleAtChange from "./job-at-change.component"; import ScheduleEventColor from "./schedule-event.color.component"; import ScheduleEventNote from "./schedule-event.note.component"; -import {useMutation} from "@apollo/client"; -import {UPDATE_APPOINTMENT} from "../../graphql/appointments.queries"; +import { useMutation } from "@apollo/client"; +import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - setScheduleContext: (context) => - dispatch(setModalContext({context: context, modal: "schedule"})), - openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), - setMessage: (text) => dispatch(setMessage(text)), + setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })), + openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), + setMessage: (text) => dispatch(setMessage(text)) }); export function ScheduleEventComponent({ - bodyshop, - setMessage, - openChatByPhone, - event, - refetch, - handleCancel, - setScheduleContext, - }) { - const {t} = useTranslation(); - const [open, setOpen] = useState(false); - const history = useNavigate(); - const searchParams = queryString.parse(useLocation().search); - const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); - const [title, setTitle] = useState(event.title); - const blockContent = ( - - setTitle(e.currentTarget.value)} - onBlur={async () => { - await updateAppointment({ - variables: { - appid: event.id, - app: { - title: title, - }, - }, - optimisticResponse: { - update_appointments: { - __typename: "appointments_mutation_response", - returning: [ - { - ...event, - title: title, - __typename: "appointments", - }, - ], - }, - }, - }); - }} - /> + bodyshop, + setMessage, + openChatByPhone, + event, + refetch, + handleCancel, + setScheduleContext +}) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const history = useNavigate(); + const searchParams = queryString.parse(useLocation().search); + const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); + const [title, setTitle] = useState(event.title); + const blockContent = ( + + setTitle(e.currentTarget.value)} + onBlur={async () => { + await updateAppointment({ + variables: { + appid: event.id, + app: { + title: title + } + }, + optimisticResponse: { + update_appointments: { + __typename: "appointments_mutation_response", + returning: [ + { + ...event, + title: title, + __typename: "appointments" + } + ] + } + } + }); + }} + /> - handleCancel({id: event.id})} - disabled={event.arrived} - > - {t("appointments.actions.unblock")} - + handleCancel({ id: event.id })} disabled={event.arrived}> + {t("appointments.actions.unblock")} + + + ); + + const popoverContent = ( + + {!event.isintake ? ( + + {event.title} + - ); - - const popoverContent = ( - - {!event.isintake ? ( - - {event.title} - - - ) : ( - - - - - + ) : ( + + + + + {`${(event.job && event.job.v_model_yr) || ""} ${ - (event.job && event.job.v_make_desc) || "" + (event.job && event.job.v_make_desc) || "" } ${(event.job && event.job.v_model_desc) || ""}`} - - - )} - - {event.job ? ( - - - {(event.job && event.job.ro_number) || ""} - - - - {(event.job && event.job.clm_total) || ""} - - - - {(event.job && event.job.ins_co_nm) || ""} - - - {(event.job && event.job.clm_no) || ""} - - - {(event.job && event.job.ownr_ea) || ""} - - - - - - - - - {(event.job && event.job.alt_transport) || ""} - - - - - ) : ( - {event.note || ""} - )} - - - {event.job ? ( - - {t("appointments.actions.viewjob")} - - ) : null} - {event.job ? ( - { - history({ - search: queryString.stringify({ - ...searchParams, - selected: event.job.id, - }), - }); - }} - > - {t("appointments.actions.preview")} - - ) : null} - {event.job ? ( - { - const Template = TemplateList("job").appointment_reminder; - GenerateDocument( - { - name: Template.key, - variables: {id: event.job.id}, - }, - { - to: event.job && event.job.ownr_ea, - subject: Template.subject, - }, - "e", - event.job && event.job.id - ); - }, - }, - { - key: "sms", - label: t("general.labels.sms"), - disabled: event.arrived || !bodyshop.messagingservicesid, - onClick: () => { - const p = parsePhoneNumber(event.job.ownr_ph1, "CA"); - if (p && p.isValid()) { - openChatByPhone({ - phone_num: p.formatInternational(), - jobid: event.job.id, - }); - setMessage( - t("appointments.labels.reminder", { - shopname: bodyshop.shopname, - date: dayjs(event.start).format("MM/DD/YYYY"), - time: dayjs(event.start).format("HH:mm a"), - }) - ); - setOpen(false); - } else { - notification["error"]({ - message: t("messaging.error.invalidphone"), - }); - } - }, - }, - ] - }} - > - {t("appointments.actions.sendreminder")} - - ) : null} - {event.arrived ? ( - handleCancel(event.id)} - disabled={event.arrived} - > - {t("appointments.actions.cancel")} - - ) : ( - { - handleCancel({id: event.id, lost_sale_reason}); - }} - > - - ({ - label: lsr, - value: lsr, - }))} - /> - - - {t("appointments.actions.cancel")} - - - } - > - handleCancel(event.id)} - disabled={event.arrived} - > - {t("appointments.actions.cancel")} - - - )} - - {event.isintake ? ( - { - setOpen(false); - setScheduleContext({ - actions: {refetch: refetch}, - context: { - jobId: event.job.id, - job: event.job, - previousEvent: event.id, - color: event.color, - alt_transport: event.job && event.job.alt_transport, - note: event.note, - }, - }); - }} - > - {t("appointments.actions.reschedule")} - - ) : ( - - )} - {event.isintake ? ( - - - {t("appointments.actions.intake")} - - - ) : null} - - - ); - - const RegularEvent = event.isintake ? ( - - {event.note && } - {`${event.job.ro_number || t("general.labels.na")}`} - - - - {`${(event.job && event.job.v_model_yr) || ""} ${ - (event.job && event.job.v_make_desc) || "" - } ${(event.job && event.job.v_model_desc) || ""}`} - - {`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${ - (event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0" - })`} - - {event.job && event.job.alt_transport && ( - {event.job.alt_transport} - )} + - ) : ( - - {`${event.title || ""}`} + )} + + {event.job ? ( + + {(event.job && event.job.ro_number) || ""} + + {(event.job && event.job.clm_total) || ""} + + + {(event.job && event.job.ins_co_nm) || ""} + + + {(event.job && event.job.clm_no) || ""} + + {(event.job && event.job.ownr_ea) || ""} + + + + + + + + {(event.job && event.job.alt_transport) || ""} + + + - ); - - return ( - !event.vacation && setOpen(vis)} - trigger="click" - content={event.block ? blockContent : popoverContent} - style={{ - height: "100%", - width: "100%", - - backgroundColor: - event.color && event.color.hex ? event.color.hex : event.color, + ) : ( + {event.note || ""} + )} + + + {event.job ? ( + + {t("appointments.actions.viewjob")} + + ) : null} + {event.job ? ( + { + history({ + search: queryString.stringify({ + ...searchParams, + selected: event.job.id + }) + }); }} - > - {RegularEvent} - - ); + > + {t("appointments.actions.preview")} + + ) : null} + {event.job ? ( + { + const Template = TemplateList("job").appointment_reminder; + GenerateDocument( + { + name: Template.key, + variables: { id: event.job.id } + }, + { + to: event.job && event.job.ownr_ea, + subject: Template.subject + }, + "e", + event.job && event.job.id + ); + } + }, + { + key: "sms", + label: t("general.labels.sms"), + disabled: event.arrived || !bodyshop.messagingservicesid, + onClick: () => { + const p = parsePhoneNumber(event.job.ownr_ph1, "CA"); + if (p && p.isValid()) { + openChatByPhone({ + phone_num: p.formatInternational(), + jobid: event.job.id + }); + setMessage( + t("appointments.labels.reminder", { + shopname: bodyshop.shopname, + date: dayjs(event.start).format("MM/DD/YYYY"), + time: dayjs(event.start).format("HH:mm a") + }) + ); + setOpen(false); + } else { + notification["error"]({ + message: t("messaging.error.invalidphone") + }); + } + } + } + ] + }} + > + {t("appointments.actions.sendreminder")} + + ) : null} + {event.arrived ? ( + handleCancel(event.id)} + disabled={event.arrived} + > + {t("appointments.actions.cancel")} + + ) : ( + { + handleCancel({ id: event.id, lost_sale_reason }); + }} + > + + ({ + label: lsr, + value: lsr + }))} + /> + + {t("appointments.actions.cancel")} + + } + > + handleCancel(event.id)} + disabled={event.arrived} + > + {t("appointments.actions.cancel")} + + + )} + + {event.isintake ? ( + { + setOpen(false); + setScheduleContext({ + actions: { refetch: refetch }, + context: { + jobId: event.job.id, + job: event.job, + previousEvent: event.id, + color: event.color, + alt_transport: event.job && event.job.alt_transport, + note: event.note + } + }); + }} + > + {t("appointments.actions.reschedule")} + + ) : ( + + )} + {event.isintake ? ( + + {t("appointments.actions.intake")} + + ) : null} + + + ); + + const RegularEvent = event.isintake ? ( + + {event.note && } + {`${event.job.ro_number || t("general.labels.na")}`} + + + + {`${(event.job && event.job.v_model_yr) || ""} ${ + (event.job && event.job.v_make_desc) || "" + } ${(event.job && event.job.v_model_desc) || ""}`} + + {`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${ + (event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0" + })`} + + {event.job && event.job.alt_transport && {event.job.alt_transport}} + + ) : ( + + {`${event.title || ""}`} + + ); + + return ( + !event.vacation && setOpen(vis)} + trigger="click" + content={event.block ? blockContent : popoverContent} + style={{ + height: "100%", + width: "100%", + + backgroundColor: event.color && event.color.hex ? event.color.hex : event.color + }} + > + {RegularEvent} + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(ScheduleEventComponent); +export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventComponent); diff --git a/client/src/components/job-at-change/schedule-event.container.jsx b/client/src/components/job-at-change/schedule-event.container.jsx index fcc85cecc..f576d4073 100644 --- a/client/src/components/job-at-change/schedule-event.container.jsx +++ b/client/src/components/job-at-change/schedule-event.container.jsx @@ -1,78 +1,73 @@ -import {useMutation} from "@apollo/client"; -import {notification} from "antd"; +import { useMutation } from "@apollo/client"; +import { notification } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {useDispatch} from "react-redux"; -import {logImEXEvent} from "../../firebase/firebase.utils"; -import {CANCEL_APPOINTMENT_BY_ID} from "../../graphql/appointments.queries"; -import {UPDATE_JOB} from "../../graphql/jobs.queries"; -import {insertAuditTrail} from "../../redux/application/application.actions"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { CANCEL_APPOINTMENT_BY_ID } from "../../graphql/appointments.queries"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import ScheduleEventComponent from "./schedule-event.component"; -export default function ScheduleEventContainer({bodyshop, event, refetch}) { - const dispatch = useDispatch(); - const {t} = useTranslation(); - const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); - const [updateJob] = useMutation(UPDATE_JOB); - const handleCancel = async ({id, lost_sale_reason}) => { - logImEXEvent("schedule_cancel_appt"); +export default function ScheduleEventContainer({ bodyshop, event, refetch }) { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); + const [updateJob] = useMutation(UPDATE_JOB); + const handleCancel = async ({ id, lost_sale_reason }) => { + logImEXEvent("schedule_cancel_appt"); - const cancelAppt = await cancelAppointment({ - variables: {appid: event.id}, - }); - notification["success"]({ - message: t("appointments.successes.canceled"), - }); + const cancelAppt = await cancelAppointment({ + variables: { appid: event.id } + }); + notification["success"]({ + message: t("appointments.successes.canceled") + }); - if (!!cancelAppt.errors) { - notification["error"]({ - message: t("appointments.errors.canceling", { - message: JSON.stringify(cancelAppt.errors), - }), - }); - return; + if (!!cancelAppt.errors) { + notification["error"]({ + message: t("appointments.errors.canceling", { + message: JSON.stringify(cancelAppt.errors) + }) + }); + return; + } + + if (event.job) { + const jobUpdate = await updateJob({ + variables: { + jobId: event.job.id, + job: { + date_scheduled: null, + scheduled_in: null, + scheduled_completion: null, + lost_sale_reason, + date_lost_sale: new Date(), + status: bodyshop.md_ro_statuses.default_imported + } } + }); + if (!jobUpdate.errors) { + dispatch( + insertAuditTrail({ + jobid: event.job.id, + operation: AuditTrailMapping.appointmentcancel(lost_sale_reason), + type: "appointmentcancel" + }) + ); + } + if (!!jobUpdate.errors) { + notification["error"]({ + message: t("jobs.errors.updating", { + message: JSON.stringify(jobUpdate.errors) + }) + }); + return; + } + } + if (refetch) refetch(); + }; - if (event.job) { - const jobUpdate = await updateJob({ - variables: { - jobId: event.job.id, - job: { - date_scheduled: null, - scheduled_in: null, - scheduled_completion: null, - lost_sale_reason, - date_lost_sale: new Date(), - status: bodyshop.md_ro_statuses.default_imported, - }, - }, - }); - if (!jobUpdate.errors) { - dispatch( - insertAuditTrail({ - jobid: event.job.id, - operation: AuditTrailMapping.appointmentcancel(lost_sale_reason), - type: "appointmentcancel",}) - ); - } - if (!!jobUpdate.errors) { - notification["error"]({ - message: t("jobs.errors.updating", { - message: JSON.stringify(jobUpdate.errors), - }), - }); - return; - } - } - if (refetch) refetch(); - }; - - return ( - - ); + return ; } diff --git a/client/src/components/job-at-change/schedule-event.note.component.jsx b/client/src/components/job-at-change/schedule-event.note.component.jsx index 951e7a271..a2eb60302 100644 --- a/client/src/components/job-at-change/schedule-event.note.component.jsx +++ b/client/src/components/job-at-change/schedule-event.note.component.jsx @@ -1,75 +1,70 @@ -import {EditFilled, SaveFilled} from "@ant-design/icons"; -import {useMutation} from "@apollo/client"; -import {Button, Input, notification, Space} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {UPDATE_APPOINTMENT} from "../../graphql/appointments.queries"; -import {selectBodyshop} from "../../redux/user/user.selectors"; +import { EditFilled, SaveFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, Input, notification, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import DataLabel from "../data-label/data-label.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ScheduleEventNote({event}) { - const [editing, setEditing] = useState(false); - const [note, setNote] = useState(event.note || ""); - const [loading, setLoading] = useState(false); - const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); - const {t} = useTranslation(); +export function ScheduleEventNote({ event }) { + const [editing, setEditing] = useState(false); + const [note, setNote] = useState(event.note || ""); + const [loading, setLoading] = useState(false); + const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); + const { t } = useTranslation(); - const toggleEdit = async () => { - if (editing) { - //Await the update - setLoading(true); - const result = await updateAppointment({ - variables: { - appid: event.id, - app: {note}, - }, - }); - - if (!!!result.errors) { - // notification["success"]({ message: t("appointments.successes.saved") }); - } else { - notification["error"]({ - message: t("jobs.errors.saving", { - error: JSON.stringify(result.errors), - }), - }); - } - - setEditing(false); - } else { - setEditing(true); + const toggleEdit = async () => { + if (editing) { + //Await the update + setLoading(true); + const result = await updateAppointment({ + variables: { + appid: event.id, + app: { note } } - setLoading(false); - }; + }); - return ( - - - {!editing ? ( - event.note || "" - ) : ( - setNote(e.target.value)} - style={{maxWidth: "8vw"}} - /> - )} - - {editing ? : } - - - - ); + if (!!!result.errors) { + // notification["success"]({ message: t("appointments.successes.saved") }); + } else { + notification["error"]({ + message: t("jobs.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } + + setEditing(false); + } else { + setEditing(true); + } + setLoading(false); + }; + + return ( + + + {!editing ? ( + event.note || "" + ) : ( + setNote(e.target.value)} style={{ maxWidth: "8vw" }} /> + )} + + {editing ? : } + + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote); diff --git a/client/src/components/job-audit-trail/job-audit-trail.component.jsx b/client/src/components/job-audit-trail/job-audit-trail.component.jsx index d9376504d..421733e48 100644 --- a/client/src/components/job-audit-trail/job-audit-trail.component.jsx +++ b/client/src/components/job-audit-trail/job-audit-trail.component.jsx @@ -1,154 +1,136 @@ -import {useQuery} from "@apollo/client"; -import {Button, Card, Col, Row, Table, Tag} from "antd"; -import {SyncOutlined} from "@ant-design/icons"; +import { useQuery } from "@apollo/client"; +import { Button, Card, Col, Row, Table, Tag } from "antd"; +import { SyncOutlined } from "@ant-design/icons"; import React from "react"; -import {useTranslation} from "react-i18next"; -import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries"; -import {DateTimeFormatter} from "../../utils/DateFormatter"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectCurrentUser} from "../../redux/user/user.selectors"; +import { useTranslation } from "react-i18next"; +import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectCurrentUser } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail); -export function JobAuditTrail({currentUser, jobId}) { - const {t} = useTranslation(); - const {loading, data, refetch} = useQuery(QUERY_AUDIT_TRAIL, { - variables: {jobid: jobId}, - skip: !jobId, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +export function JobAuditTrail({ currentUser, jobId }) { + const { t } = useTranslation(); + const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, { + variables: { jobid: jobId }, + skip: !jobId, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); - const columns = [ - { - title: t("audit.fields.created"), - dataIndex: "created", - key: "created", + const columns = [ + { + title: t("audit.fields.created"), + dataIndex: "created", + key: "created", + render: (text, record) => {record.created} + }, + { + title: t("audit.fields.useremail"), + dataIndex: "useremail", + key: "useremail" + }, + { + title: t("audit.fields.operation"), + dataIndex: "operation", + key: "operation" + } + ]; + const emailColumns = [ + { + title: t("audit.fields.created"), + dataIndex: " created_at", + key: " created_at", + + render: (text, record) => {record.created_at} + }, + + { + title: t("audit.fields.useremail"), + dataIndex: "useremail", + key: "useremail" + }, + + { + title: t("audit.fields.to"), + dataIndex: "to", + key: "to", + + render: (text, record) => record.to && record.to.map((email, idx) => {email}) + }, + { + title: t("audit.fields.cc"), + dataIndex: "cc", + key: "cc", + + render: (text, record) => record.cc && record.cc.map((email, idx) => {email}) + }, + { + title: t("audit.fields.subject"), + dataIndex: "subject", + key: "subject" + }, + { + title: t("audit.fields.status"), + dataIndex: "status", + key: "status" + }, + ...(currentUser?.email.includes("@imex.") + ? [ + { + title: t("audit.fields.contents"), + dataIndex: "contents", + key: "contents", + width: "10%", render: (text, record) => ( - {record.created} - ), - }, - { - title: t("audit.fields.useremail"), - dataIndex: "useremail", - key: "useremail", - }, - { - title: t("audit.fields.operation"), - dataIndex: "operation", - key: "operation", - }, - ]; - const emailColumns = [ - { - title: t("audit.fields.created"), - dataIndex: " created_at", - key: " created_at", - - render: (text, record) => ( - {record.created_at} - ), - }, - - { - title: t("audit.fields.useremail"), - dataIndex: "useremail", - key: "useremail", - }, - - { - title: t("audit.fields.to"), - dataIndex: "to", - key: "to", - - render: (text, record) => - record.to && - record.to.map((email, idx) => {email}), - }, - { - title: t("audit.fields.cc"), - dataIndex: "cc", - key: "cc", - - render: (text, record) => - record.cc && - record.cc.map((email, idx) => {email}), - }, - { - title: t("audit.fields.subject"), - dataIndex: "subject", - key: "subject", - }, - { - title: t("audit.fields.status"), - dataIndex: "status", - key: "status", - }, - ...(currentUser?.email.includes("@imex.") - ? [ - { - title: t("audit.fields.contents"), - dataIndex: "contents", - key: "contents", - width: "10%", - render: (text, record) => ( - { - var win = window.open( - "", - "Title", - "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=400," - ); - win.document.body.innerHTML = record.contents; - }} - > - Preview - - ), - }, - ] - : []), - ]; - return ( - - - { - refetch(); - }} - > - - - } - > - - - - - - - - - - ); + { + var win = window.open( + "", + "Title", + "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=400," + ); + win.document.body.innerHTML = record.contents; + }} + > + Preview + + ) + } + ] + : []) + ]; + return ( + + + { + refetch(); + }} + > + + + } + > + + + + + + + + + + ); } diff --git a/client/src/components/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx index 581a9a8b8..85f8ed893 100644 --- a/client/src/components/job-bills-total/job-bills-total.component.jsx +++ b/client/src/components/job-bills-total/job-bills-total.component.jsx @@ -1,295 +1,270 @@ -import {Card, Col, Row, Space, Statistic, Tooltip, Typography} from "antd"; +import { Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd"; import Dinero from "dinero.js"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import AlertComponent from "../alert/alert.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import "./job-bills-total.styles.scss"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; -export default function JobBillsTotalComponent({ - loading, - bills, - partsOrders, - jobTotals, - }) { - const {t} = useTranslation(); +export default function JobBillsTotalComponent({ loading, bills, partsOrders, jobTotals }) { + const { t } = useTranslation(); - if (loading) return ; - if (!!!jobTotals) - return ( - + if (loading) return ; + if (!!!jobTotals) return ; + + const totals = jobTotals; + + let billTotals = Dinero(); + let billCms = Dinero(); + let lbrAdjustments = Dinero(); + let totalReturns = Dinero(); + let totalReturnsMarkedNotReceived = Dinero(); + let totalReturnsMarkedReceived = Dinero(); + + partsOrders.forEach((p) => + p.parts_order_lines.forEach((pol) => { + if (p.return) { + totalReturns = totalReturns.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100) + }).multiply(pol.quantity) ); - const totals = jobTotals; + if (pol.cm_received === null) { + return; //TODO:AIO This was previously removed. Check if functionality impacted. + // Skip this calculation for bills posted prior to the CNR change. + } else { + if (pol.cm_received === false) { + totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100) + }).multiply(pol.quantity) + ); + } else { + totalReturnsMarkedReceived = totalReturnsMarkedReceived.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100) + }).multiply(pol.quantity) + ); + } + } + } + }) + ); - let billTotals = Dinero(); - let billCms = Dinero(); - let lbrAdjustments = Dinero(); - let totalReturns = Dinero(); - let totalReturnsMarkedNotReceived = Dinero(); - let totalReturnsMarkedReceived = Dinero(); + bills.forEach((i) => + i.billlines.forEach((il) => { + if (!i.is_credit_memo) { + billTotals = billTotals.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100) + }).multiply(il.quantity) + ); + } else { + billCms = billCms.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100) + }).multiply(il.quantity) + ); + } + if (il.deductedfromlbr) { + lbrAdjustments = lbrAdjustments.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100) + }).multiply(il.quantity) + ); + } + }) + ); - partsOrders.forEach((p) => - p.parts_order_lines.forEach((pol) => { - if (p.return) { - totalReturns = totalReturns.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); + const totalPartsSublet = Dinero(totals.parts.parts.total) + .add(Dinero(totals.parts.sublets.total)) + .add(Dinero(totals.additional.shipping)) + .add(Dinero(totals.additional.towing)) + .add( + InstanceRenderManager({ + imex: Dinero(), + rome: Dinero(totals.additional.additionalCosts), + promanager: "USE_ROME" + }) + ); // Additional costs were captured for Rome, but not imex. - if (pol.cm_received === null) { - return; //TODO:AIO This was previously removed. Check if functionality impacted. - // Skip this calculation for bills posted prior to the CNR change. - } else { - if (pol.cm_received === false) { - totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); - } else { - totalReturnsMarkedReceived = totalReturnsMarkedReceived.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); - } + const discrepancy = totalPartsSublet.subtract(billTotals); + + const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); + + const discrepWithCms = discrepWithLbrAdj.add(totalReturns); + const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. + + return ( + + + + + + } + > + + + - + + } + > + + + = + + } + > + + + + + + } + > + + + = + + } + > + + + + + + } + > + + + = + + } + > + + + + + + + + + + } + > + + + + } + > + = 0 + ? calculatedCreditsNotReceived.toFormat() + : Dinero().toFormat() } - } - }) - ); - - bills.forEach((i) => - i.billlines.forEach((il) => { - if (!i.is_credit_memo) { - billTotals = billTotals.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } else { - billCms = billCms.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } - if (il.deductedfromlbr) { - lbrAdjustments = lbrAdjustments.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } - }) - ); - - const totalPartsSublet = Dinero(totals.parts.parts.total) - .add(Dinero(totals.parts.sublets.total)) - .add(Dinero(totals.additional.shipping)) - .add(Dinero(totals.additional.towing)) - .add( InstanceRenderManager({imex: Dinero(), rome: Dinero(totals.additional.additionalCosts),promanager: "USE_ROME" })) ; // Additional costs were captured for Rome, but not imex. - - const discrepancy = totalPartsSublet.subtract(billTotals); - - const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); - - const discrepWithCms = discrepWithLbrAdj.add(totalReturns); - const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. - - return ( - - - - - - } - > - - - - - - } - > - - - = - - } - > - - - + - - } - > - - - = - - } - > - - - + - - } - > - - - = - - } - > - - - - - - - - - - } - > - - - - } - > - = 0 - ? calculatedCreditsNotReceived.toFormat() - : Dinero().toFormat() - } - /> - - - } - > - = 0 - ? totalReturnsMarkedNotReceived.toFormat() - : Dinero().toFormat() - } - /> - - - - - - ); + /> + + + } + > + = 0 + ? totalReturnsMarkedNotReceived.toFormat() + : Dinero().toFormat() + } + /> + + + + + + ); } diff --git a/client/src/components/job-calculate-totals/job-calculate-totals.component.jsx b/client/src/components/job-calculate-totals/job-calculate-totals.component.jsx index 3ea7386de..706cf16a0 100644 --- a/client/src/components/job-calculate-totals/job-calculate-totals.component.jsx +++ b/client/src/components/job-calculate-totals/job-calculate-totals.component.jsx @@ -1,60 +1,60 @@ -import {Button, notification} from "antd"; +import { Button, notification } from "antd"; import Axios from "axios"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; -export default function JobCalculateTotals({job, disabled, refetch}) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); +export default function JobCalculateTotals({ job, disabled, refetch }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); - const handleCalculate = async () => { - try { - setLoading(true); + const handleCalculate = async () => { + try { + setLoading(true); - await Axios.post("/job/totalsssu", { - id: job.id, - }); + await Axios.post("/job/totalsssu", { + id: job.id + }); - if (refetch) refetch(); - // const result = await updateJob({ - // refetchQueries: ["GET_JOB_BY_PK"], - // awaitRefetchQueries: true, - // variables: { - // jobId: job.id, - // job: { - // job_totals: newTotals, - // clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"), - // owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat( - // "0.00" - // ), - // }, - // }, - // }); - // if (!!!result.errors) { - // notification["success"]({ message: t("jobs.successes.updated") }); - // } else { - // notification["error"]({ - // message: t("jobs.errors.updating", { - // error: JSON.stringify(result.errors), - // }), - // }); - // } - } catch (error) { - notification["error"]({ - message: t("jobs.errors.updating", { - error: JSON.stringify(error), - }), - }); - } finally { - setLoading(false); - } - }; + if (refetch) refetch(); + // const result = await updateJob({ + // refetchQueries: ["GET_JOB_BY_PK"], + // awaitRefetchQueries: true, + // variables: { + // jobId: job.id, + // job: { + // job_totals: newTotals, + // clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"), + // owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat( + // "0.00" + // ), + // }, + // }, + // }); + // if (!!!result.errors) { + // notification["success"]({ message: t("jobs.successes.updated") }); + // } else { + // notification["error"]({ + // message: t("jobs.errors.updating", { + // error: JSON.stringify(result.errors), + // }), + // }); + // } + } catch (error) { + notification["error"]({ + message: t("jobs.errors.updating", { + error: JSON.stringify(error) + }) + }); + } finally { + setLoading(false); + } + }; - return ( - - - {t("jobs.actions.recalculate")} - - - ); + return ( + + + {t("jobs.actions.recalculate")} + + + ); } diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index 881912223..2f901d818 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -1,331 +1,296 @@ -import {useMutation} from "@apollo/client"; -import {Button, Card, Form, Input, notification, Switch} from "antd"; +import { useMutation } from "@apollo/client"; +import { Button, Card, Form, Input, notification, Switch } from "antd"; import dayjs from "../../../../utils/day"; import queryString from "query-string"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useLocation, useNavigate, useParams} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {logImEXEvent} from "../../../../firebase/firebase.utils"; -import {MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED,} from "../../../../graphql/appointments.queries"; -import {UPDATE_JOB} from "../../../../graphql/jobs.queries"; -import {UPDATE_OWNER} from "../../../../graphql/owners.queries"; -import {insertAuditTrail} from "../../../../redux/application/application.actions"; -import {selectBodyshop, selectCurrentUser,} from "../../../../redux/user/user.selectors"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../../../firebase/firebase.utils"; +import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries"; +import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; +import { UPDATE_OWNER } from "../../../../graphql/owners.queries"; +import { insertAuditTrail } from "../../../../redux/application/application.actions"; +import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors"; import AuditTrailMapping from "../../../../utils/AuditTrailMappings"; import ConfigFormComponents from "../../../config-form-components/config-form-components.component"; import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - insertAuditTrail: ({jobid, operation, type}) => - dispatch(insertAuditTrail({jobid, operation, type })), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); -export function JobChecklistForm({ - insertAuditTrail, - formItems, - bodyshop, - currentUser, - type, - job, - readOnly = false, - }) { - const {t} = useTranslation(); - const [intakeJob] = useMutation(UPDATE_JOB); - const [loading, setLoading] = useState(false); - const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED); - const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED); - const [updateOwner] = useMutation(UPDATE_OWNER); +export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, currentUser, type, job, readOnly = false }) { + const { t } = useTranslation(); + const [intakeJob] = useMutation(UPDATE_JOB); + const [loading, setLoading] = useState(false); + const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED); + const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED); + const [updateOwner] = useMutation(UPDATE_OWNER); - const {jobId} = useParams(); - const history = useNavigate(); - const search = queryString.parse(useLocation().search); - const [form] = Form.useForm(); + const { jobId } = useParams(); + const history = useNavigate(); + const search = queryString.parse(useLocation().search); + const [form] = Form.useForm(); - const handleFinish = async (values) => { - setLoading(true); - logImEXEvent("job_complete_intake"); + const handleFinish = async (values) => { + setLoading(true); + logImEXEvent("job_complete_intake"); - const result = await intakeJob({ - variables: { - jobId: jobId, - job: { - ...(type === "intake" && {inproduction: values.addToProduction}), - status: - (type === "intake" && bodyshop.md_ro_statuses.default_arrived) || - (type === "deliver" && bodyshop.md_ro_statuses.default_delivered), - ...(type === "intake" && {actual_in: new Date()}), - ...(type === "intake" && { - production_vars: { - ...(job ? job.production_vars : {}), + const result = await intakeJob({ + variables: { + jobId: jobId, + job: { + ...(type === "intake" && { inproduction: values.addToProduction }), + status: + (type === "intake" && bodyshop.md_ro_statuses.default_arrived) || + (type === "deliver" && bodyshop.md_ro_statuses.default_delivered), + ...(type === "intake" && { actual_in: new Date() }), + ...(type === "intake" && { + production_vars: { + ...(job ? job.production_vars : {}), - note: - values.production_vars && - values.production_vars.note && - values.production_vars.note !== "" - ? values && - values.production_vars && - values.production_vars.note - : job && job.production_vars && job.production_vars.note, - }, - }), - ...(type === "intake" && { - scheduled_completion: values.scheduled_completion, - }), - ...(type === "intake" && - bodyshop.intakechecklist && - bodyshop.intakechecklist.next_contact_hours && - bodyshop.intakechecklist.next_contact_hours > 0 && { - date_next_contact: dayjs().add( - bodyshop.intakechecklist.next_contact_hours, - "hour" - ), - }), - ...(type === "deliver" && { - actual_completion: values.actual_completion, - }), + note: + values.production_vars && values.production_vars.note && values.production_vars.note !== "" + ? values && values.production_vars && values.production_vars.note + : job && job.production_vars && job.production_vars.note + } + }), + ...(type === "intake" && { + scheduled_completion: values.scheduled_completion + }), + ...(type === "intake" && + bodyshop.intakechecklist && + bodyshop.intakechecklist.next_contact_hours && + bodyshop.intakechecklist.next_contact_hours > 0 && { + date_next_contact: dayjs().add(bodyshop.intakechecklist.next_contact_hours, "hour") + }), + ...(type === "deliver" && { + actual_completion: values.actual_completion + }), - [(type === "intake" && "intakechecklist") || - (type === "deliver" && "deliverchecklist")]: { - ...values, - form: formItems.map((fi) => { - return { - ...fi, - value: values[fi.name], - }; - }), - completed_by: currentUser.email, - completed_at: new Date(), - }, + [(type === "intake" && "intakechecklist") || (type === "deliver" && "deliverchecklist")]: { + ...values, + form: formItems.map((fi) => { + return { + ...fi, + value: values[fi.name] + }; + }), + completed_by: currentUser.email, + completed_at: new Date() + }, - ...(type === "intake" && - values.scheduled_delivery && { - scheduled_delivery: values.scheduled_delivery, - }), - ...(type === "deliver" && { - scheduled_delivery: values.scheduled_delivery, - actual_delivery: values.actual_delivery, - }), - ...(type === "deliver" && - values.removeFromProduction && { - inproduction: false, - }), - }, - }, + ...(type === "intake" && + values.scheduled_delivery && { + scheduled_delivery: values.scheduled_delivery + }), + ...(type === "deliver" && { + scheduled_delivery: values.scheduled_delivery, + actual_delivery: values.actual_delivery + }), + ...(type === "deliver" && + values.removeFromProduction && { + inproduction: false + }) + } + } + }); + if (!!search.appointmentId) { + const appUpdate = await markAptArrived({ + variables: { appointmentId: search.appointmentId } + }); + + if (!!appUpdate.errors) { + notification["error"]({ + message: t("checklist.errors.complete", { + error: JSON.stringify(result.errors) + }) }); - if (!!search.appointmentId) { - const appUpdate = await markAptArrived({ - variables: {appointmentId: search.appointmentId}, - }); + } + } else if (type === "intake" && !search.appointmentId) { + const appUpdate = await markLatestAptArrived({ + variables: { jobId: jobId } + }); - if (!!appUpdate.errors) { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors), - }), - }); - } - } else if (type === "intake" && !search.appointmentId) { - const appUpdate = await markLatestAptArrived({ - variables: {jobId: jobId}, - }); + if (!!appUpdate.errors) { + notification["error"]({ + message: t("checklist.errors.complete", { + error: JSON.stringify(result.errors) + }) + }); + } + } - if (!!appUpdate.errors) { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors), - }), - }); - } + if (type === "intake" && job.owner && job.owner.id) { + //Updae Owner Allow to Text + const updateOwnerResult = await updateOwner({ + variables: { + ownerId: job.owner.id, + owner: { allow_text_message: values.allow_text_message } } + }); - if (type === "intake" && job.owner && job.owner.id) { - //Updae Owner Allow to Text - const updateOwnerResult = await updateOwner({ - variables: { - ownerId: job.owner.id, - owner: {allow_text_message: values.allow_text_message}, - }, - }); + if (!!updateOwnerResult.errors) { + notification["error"]({ + message: t("checklist.errors.complete", { + error: JSON.stringify(result.errors) + }) + }); + } + } - if (!!updateOwnerResult.errors) { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors), - }), - }); - } - } + setLoading(false); - setLoading(false); + if (!!!result.errors) { + notification["success"]({ message: t("checklist.successes.completed") }); + history(`/manage/jobs/${jobId}`); - if (!!!result.errors) { - notification["success"]({message: t("checklist.successes.completed")}); - history(`/manage/jobs/${jobId}`); + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobchecklist( + type, + (type === "deliver" && values.removeFromProduction && false) || (type === "intake" && values.addToProduction), + (type === "intake" && bodyshop.md_ro_statuses.default_arrived) || + (type === "deliver" && bodyshop.md_ro_statuses.default_delivered) + ), + type: "jobchecklist" + }); + } else { + notification["error"]({ + message: t("checklist.errors.complete", { + error: JSON.stringify(result.errors) + }) + }); + } + }; - insertAuditTrail({ - jobid: jobId, - operation: AuditTrailMapping.jobchecklist( - type, - (type === "deliver" && values.removeFromProduction && false) || - (type === "intake" && values.addToProduction), - (type === "intake" && bodyshop.md_ro_statuses.default_arrived) || - (type === "deliver" && bodyshop.md_ro_statuses.default_delivered) - ), - type: "jobchecklist",}); - } else { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors), - }), - }); - } - }; + return ( + form.submit()}> + {t("general.actions.submit")} + + ) + } + > + fi.value) + .reduce((acc, fi) => { + acc[fi.name] = fi.value; + return acc; + }, {}) + }} + > + - return ( - form.submit()} - > - {t("general.actions.submit")} - - ) - } - > - fi.value) - .reduce((acc, fi) => { - acc[fi.name] = fi.value; - return acc; - }, {}), - }} + {type === "intake" && ( + + - - - {type === "intake" && ( - - - - - - - - - - - - - - - - - - )} - {type === "deliver" && ( - - - - - - - - - - - - )} - - - ); + + + + + + + + + + + + + + + + )} + {type === "deliver" && ( + + + + + + + + + + + + )} + + + ); } export default connect(mapStateToProps, mapDispatchToProps)(JobChecklistForm); diff --git a/client/src/components/job-checklist/components/job-checklist-template-list/job-checklist-template-list.component.jsx b/client/src/components/job-checklist/components/job-checklist-template-list/job-checklist-template-list.component.jsx index 81f6a3f56..05fa55c77 100644 --- a/client/src/components/job-checklist/components/job-checklist-template-list/job-checklist-template-list.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-template-list/job-checklist-template-list.component.jsx @@ -1,78 +1,72 @@ -import {PrinterFilled} from "@ant-design/icons"; -import {Button, Card, List} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {useParams} from "react-router-dom"; -import {logImEXEvent} from "../../../../firebase/firebase.utils"; -import {GenerateDocument, GenerateDocuments,} from "../../../../utils/RenderTemplate"; -import {TemplateList} from "../../../../utils/TemplateConstants"; +import { PrinterFilled } from "@ant-design/icons"; +import { Button, Card, List } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { logImEXEvent } from "../../../../firebase/firebase.utils"; +import { GenerateDocument, GenerateDocuments } from "../../../../utils/RenderTemplate"; +import { TemplateList } from "../../../../utils/TemplateConstants"; const TemplateListGenerated = TemplateList(); -export default function JobIntakeTemplateList({templates}) { - const {jobId} = useParams(); - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); +export default function JobIntakeTemplateList({ templates }) { + const { jobId } = useParams(); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); - const renderTemplate = async (templateKey) => { - setLoading(true); - logImEXEvent("job_checklist_template_render"); + const renderTemplate = async (templateKey) => { + setLoading(true); + logImEXEvent("job_checklist_template_render"); - await GenerateDocument( - { - name: templateKey, - variables: {id: jobId}, - }, - {}, - "p" - ); - setLoading(false); - }; - - const renderAllTemplates = async () => { - logImEXEvent("checklist_render_all_templates"); - setLoading(true); - - await GenerateDocuments( - templates.map((key) => { - return {name: key, variables: {id: jobId}}; - }) - ); - setLoading(false); - }; - - return ( - - {t("checklist.actions.printall")} - - } - > - ( - renderTemplate(template)} - > - - , - ]} - > - - - )} - /> - + await GenerateDocument( + { + name: templateKey, + variables: { id: jobId } + }, + {}, + "p" ); + setLoading(false); + }; + + const renderAllTemplates = async () => { + logImEXEvent("checklist_render_all_templates"); + setLoading(true); + + await GenerateDocuments( + templates.map((key) => { + return { name: key, variables: { id: jobId } }; + }) + ); + setLoading(false); + }; + + return ( + + {t("checklist.actions.printall")} + + } + > + ( + renderTemplate(template)}> + + + ]} + > + + + )} + /> + + ); } diff --git a/client/src/components/job-checklist/job-checklist-display.component.jsx b/client/src/components/job-checklist/job-checklist-display.component.jsx index 83b087b7e..e664c2bab 100644 --- a/client/src/components/job-checklist/job-checklist-display.component.jsx +++ b/client/src/components/job-checklist/job-checklist-display.component.jsx @@ -1,11 +1,11 @@ import React from "react"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; -export default function JobChecklistDisplay({checklist}) { - if (!checklist) return ; - return ( - - - - ); +export default function JobChecklistDisplay({ checklist }) { + if (!checklist) return ; + return ( + + + + ); } diff --git a/client/src/components/job-checklist/job-checklist.component.jsx b/client/src/components/job-checklist/job-checklist.component.jsx index c2357bf54..fe54277a9 100644 --- a/client/src/components/job-checklist/job-checklist.component.jsx +++ b/client/src/components/job-checklist/job-checklist.component.jsx @@ -1,19 +1,19 @@ import React from "react"; import JobChecklistTemplateList from "./components/job-checklist-template-list/job-checklist-template-list.component"; import JobChecklistForm from "./components/job-checklist-form/job-checklist-form.component"; -import {Col, Row} from "antd"; +import { Col, Row } from "antd"; -export default function JobIntakeComponent({checklistConfig, type, job}) { - const {form, templates} = checklistConfig; +export default function JobIntakeComponent({ checklistConfig, type, job }) { + const { form, templates } = checklistConfig; - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); } diff --git a/client/src/components/job-costing-modal/job-costing-modal.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.component.jsx index 9d7ace44f..5eded72b9 100644 --- a/client/src/components/job-costing-modal/job-costing-modal.component.jsx +++ b/client/src/components/job-costing-modal/job-costing-modal.component.jsx @@ -1,32 +1,27 @@ -import {Typography} from "antd"; +import { Typography } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component"; import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component"; import JobCostingPie from "./job-costing-modal.pie.component"; -export default function JobCostingModalComponent({ - summaryData, - costCenterData, - }) { - const {t} = useTranslation(); +export default function JobCostingModalComponent({ summaryData, costCenterData }) { + const { t } = useTranslation(); - return ( - - - - - - - {t("jobs.labels.sales")} - - - - - {t("jobs.labels.cost")} - - - + return ( + + + + + + {t("jobs.labels.sales")} + - ); + + {t("jobs.labels.cost")} + + + + + ); } diff --git a/client/src/components/job-costing-modal/job-costing-modal.container.jsx b/client/src/components/job-costing-modal/job-costing-modal.container.jsx index a1d8f764d..31fd7f949 100644 --- a/client/src/components/job-costing-modal/job-costing-modal.container.jsx +++ b/client/src/components/job-costing-modal/job-costing-modal.container.jsx @@ -1,72 +1,63 @@ -import {Modal} from "antd"; +import { Modal } from "antd"; import axios from "axios"; -import React, {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {toggleModalVisible} from "../../redux/modals/modals.actions"; -import {selectJobCosting} from "../../redux/modals/modals.selectors"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectJobCosting } from "../../redux/modals/modals.selectors"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import JobCostingModalComponent from "./job-costing-modal.component"; const mapStateToProps = createStructuredSelector({ - jobCostingModal: selectJobCosting, + jobCostingModal: selectJobCosting }); const mapDispatchToProps = (dispatch) => ({ - toggleModalVisible: () => dispatch(toggleModalVisible("jobCosting")), + toggleModalVisible: () => dispatch(toggleModalVisible("jobCosting")) }); -export function JobCostingModalContainer({ - jobCostingModal, - toggleModalVisible, - }) { - const {t} = useTranslation(); - const [costingData, setCostingData] = useState(null); - const {open, context} = jobCostingModal; - const {jobId} = context; +export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }) { + const { t } = useTranslation(); + const [costingData, setCostingData] = useState(null); + const { open, context } = jobCostingModal; + const { jobId } = context; - useEffect(() => { - async function getData() { - if (jobId && open) { - const {data} = await axios.post("/job/costing", {jobid: jobId}); + useEffect(() => { + async function getData() { + if (jobId && open) { + const { data } = await axios.post("/job/costing", { jobid: jobId }); - setCostingData(data); - } - } + setCostingData(data); + } + } - getData(); - }, [jobId, open]); + getData(); + }, [jobId, open]); - return ( - { - toggleModalVisible(); - setCostingData(null); - }} - onCancel={() => { - toggleModalVisible(); - setCostingData(null); - }} - cancelButtonProps={{style: {display: "none"}}} - width="90%" - destroyOnClose - > - {!costingData ? ( - - ) : ( - - )} - - ); + return ( + { + toggleModalVisible(); + setCostingData(null); + }} + onCancel={() => { + toggleModalVisible(); + setCostingData(null); + }} + cancelButtonProps={{ style: { display: "none" } }} + width="90%" + destroyOnClose + > + {!costingData ? ( + + ) : ( + + )} + + ); } -export default connect( - mapStateToProps, - mapDispatchToProps -)(JobCostingModalContainer); +export default connect(mapStateToProps, mapDispatchToProps)(JobCostingModalContainer); diff --git a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx index 9e7f17344..736372740 100644 --- a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx +++ b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx @@ -1,68 +1,54 @@ -import React, {useCallback, useMemo} from "react"; -import {Cell, Pie, PieChart, ResponsiveContainer} from "recharts"; +import React, { useCallback, useMemo } from "react"; +import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts"; import Dinero from "dinero.js"; -export default function JobCostingPieComponent({ - type = "sales", - costCenterData, - }) { - const Calculatedata = useCallback( - (data) => { - if (data && data.length > 0) { - return data.reduce((acc, i) => { - const value = - type === "sales" - ? Dinero(i.sales_dinero).getAmount() - : Dinero(i.costs_dinero).getAmount(); +export default function JobCostingPieComponent({ type = "sales", costCenterData }) { + const Calculatedata = useCallback( + (data) => { + if (data && data.length > 0) { + return data.reduce((acc, i) => { + const value = type === "sales" ? Dinero(i.sales_dinero).getAmount() : Dinero(i.costs_dinero).getAmount(); - if (value > 0) { - acc.push({ - name: i.cost_center, - color: "#" + Math.floor(Math.random() * 16777215).toString(16), + if (value > 0) { + acc.push({ + name: i.cost_center, + color: "#" + Math.floor(Math.random() * 16777215).toString(16), - label: `${i.cost_center} - ${ - type === "sales" - ? Dinero(i.sales_dinero).toFormat() - : Dinero(i.costs_dinero).toFormat() - }`, - value: - type === "sales" - ? Dinero(i.sales_dinero).getAmount() - : Dinero(i.costs_dinero).getAmount(), - }); - } - return acc; - }, []); - } else { - return []; - } - }, - [type] - ); + label: `${i.cost_center} - ${ + type === "sales" ? Dinero(i.sales_dinero).toFormat() : Dinero(i.costs_dinero).toFormat() + }`, + value: type === "sales" ? Dinero(i.sales_dinero).getAmount() : Dinero(i.costs_dinero).getAmount() + }); + } + return acc; + }, []); + } else { + return []; + } + }, + [type] + ); - const memoizedData = useMemo(() => Calculatedata(costCenterData), [ - costCenterData, - Calculatedata, - ]); + const memoizedData = useMemo(() => Calculatedata(costCenterData), [costCenterData, Calculatedata]); - return ( - - - entry.label} - labelLine - > - {memoizedData.map((entry, index) => ( - - ))} - - - - ); + return ( + + + entry.label} + labelLine + > + {memoizedData.map((entry, index) => ( + + ))} + + + + ); } diff --git a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx index 3194a8206..56e8baf85 100644 --- a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx +++ b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx @@ -1,129 +1,105 @@ -import {Input, Space, Table, Typography} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {alphaSort} from "../../utils/sorters"; +import { Input, Space, Table, Typography } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { alphaSort } from "../../utils/sorters"; import Dinero from "dinero.js"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; -export default function JobCostingPartsTable({data, summaryData}) { - const [searchText, setSearchText] = useState(""); - const [state, setState] = useState({ - sortedInfo: {}, - }); +export default function JobCostingPartsTable({ data, summaryData }) { + const [searchText, setSearchText] = useState(""); + const [state, setState] = useState({ + sortedInfo: {} + }); - const handleTableChange = (pagination, filters, sorter) => { - setState({...state, filteredInfo: filters, sortedInfo: sorter}); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; - const {t} = useTranslation(); + const { t } = useTranslation(); - const columns = [ - { - title: t("bodyshop.fields.responsibilitycenter"), - dataIndex: "cost_center", - key: "cost_center", - sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), - sortOrder: - state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order, - }, - { - title: t("jobs.labels.sales"), - dataIndex: "sales", - key: "sales", - sorter: (a, b) => - parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "sales" && state.sortedInfo.order, - }, + const columns = [ + { + title: t("bodyshop.fields.responsibilitycenter"), + dataIndex: "cost_center", + key: "cost_center", + sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), + sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order + }, + { + title: t("jobs.labels.sales"), + dataIndex: "sales", + key: "sales", + sorter: (a, b) => parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)), + sortOrder: state.sortedInfo.columnKey === "sales" && state.sortedInfo.order + }, - { - title: t("jobs.labels.costs"), - dataIndex: "costs", - key: "costs", - sorter: (a, b) => - parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "costs" && state.sortedInfo.order, - }, + { + title: t("jobs.labels.costs"), + dataIndex: "costs", + key: "costs", + sorter: (a, b) => parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)), + sortOrder: state.sortedInfo.columnKey === "costs" && state.sortedInfo.order + }, - { - title: t("jobs.labels.gpdollars"), - dataIndex: "gpdollars", - key: "gpdollars", - sorter: (a, b) => - parseFloat(a.gpdollars.substring(1)) - - parseFloat(b.gpdollars.substring(1)), + { + title: t("jobs.labels.gpdollars"), + dataIndex: "gpdollars", + key: "gpdollars", + sorter: (a, b) => parseFloat(a.gpdollars.substring(1)) - parseFloat(b.gpdollars.substring(1)), - sortOrder: - state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order, - }, - { - title: t("jobs.labels.gppercent"), - dataIndex: "gppercent", - key: "gppercent", - sorter: (a, b) => - parseFloat(a.gppercent.slice(0, -1) || 0) - - parseFloat(b.gppercent.slice(0, -1) || 0), - sortOrder: - state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order, - }, - ]; + sortOrder: state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order + }, + { + title: t("jobs.labels.gppercent"), + dataIndex: "gppercent", + key: "gppercent", + sorter: (a, b) => parseFloat(a.gppercent.slice(0, -1) || 0) - parseFloat(b.gppercent.slice(0, -1) || 0), + sortOrder: state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order + } + ]; - const filteredData = - searchText === "" - ? data - : data.filter((d) => - (d.cost_center || "") - .toString() - .toLowerCase() - .includes(searchText.toLowerCase()) - ); + const filteredData = + searchText === "" + ? data + : data.filter((d) => (d.cost_center || "").toString().toLowerCase().includes(searchText.toLowerCase())); - return ( - - { - return ( - - { - e.preventDefault(); - setSearchText(e.target.value); - }} - /> - - ); + return ( + + { + return ( + + { + e.preventDefault(); + setSearchText(e.target.value); }} - scroll={{ - x: "50%", //y: "40rem" - }} - onChange={handleTableChange} - pagination={{position: "top", defaultPageSize: pageLimit}} - columns={columns} - rowKey="id" - dataSource={filteredData} - summary={() => ( - - - - {t("general.labels.totals")} - - - - {Dinero(summaryData.totalSales).toFormat()} - - - {Dinero(summaryData.totalCost).toFormat()} - - - {Dinero(summaryData.gpdollars).toFormat()} - - - - )} - /> - - ); + /> + + ); + }} + scroll={{ + x: "50%" //y: "40rem" + }} + onChange={handleTableChange} + pagination={{ position: "top", defaultPageSize: pageLimit }} + columns={columns} + rowKey="id" + dataSource={filteredData} + summary={() => ( + + + {t("general.labels.totals")} + + {Dinero(summaryData.totalSales).toFormat()} + {Dinero(summaryData.totalCost).toFormat()} + {Dinero(summaryData.gpdollars).toFormat()} + + + )} + /> + + ); } diff --git a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx index fc06e40eb..38a832d91 100644 --- a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx +++ b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx @@ -1,63 +1,33 @@ -import {Statistic} from "antd"; +import { Statistic } from "antd"; import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; import Dinero from "dinero.js"; -export default function JobCostingStatistics({summaryData}) { - const {t} = useTranslation(); +export default function JobCostingStatistics({ summaryData }) { + const { t } = useTranslation(); - return ( - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + ); } diff --git a/client/src/components/job-create-iou/job-create-iou.component.jsx b/client/src/components/job-create-iou/job-create-iou.component.jsx index 6fee70d89..0aed7b2ea 100644 --- a/client/src/components/job-create-iou/job-create-iou.component.jsx +++ b/client/src/components/job-create-iou/job-create-iou.component.jsx @@ -1,98 +1,95 @@ -import {useApolloClient} from "@apollo/client"; -import {useSplitTreatments} from "@splitsoftware/splitio-react"; -import {Button, notification, Popconfirm} from "antd"; -import React, {useState} from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {useNavigate} from "react-router-dom"; -import {createStructuredSelector} from "reselect"; -import {UPDATE_JOB_LINES_IOU} from "../../graphql/jobs-lines.queries"; -import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; -import {CreateIouForJob} from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; -import {selectTechnician} from "../../redux/tech/tech.selectors"; +import { useApolloClient } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { Button, notification, Popconfirm } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - currentUser: selectCurrentUser, - technician: selectTechnician, + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + technician: selectTechnician }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU); -export function JobCreateIOU({bodyshop, currentUser, job, selectedJobLines, technician}) { - const {t} = useTranslation(); - const [loading, setLoading] = useState(false); - const client = useApolloClient(); - const history = useNavigate(); +export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines, technician }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const client = useApolloClient(); + const history = useNavigate(); + const { + treatments: { IOU_Tracking } + } = useSplitTreatments({ + attributes: {}, + names: ["IOU_Tracking"], + splitKey: bodyshop.imexshopid + }); - const {treatments: {IOU_Tracking}} = useSplitTreatments({ - attributes: {}, - names: ["IOU_Tracking"], - splitKey: bodyshop.imexshopid, + if (IOU_Tracking.treatment !== "on") return null; + + const handleCreateIou = async () => { + setLoading(true); + //Query all of the job details to recreate. + const iouId = await CreateIouForJob( + client, + job.id, + { + status: bodyshop.md_ro_statuses.default_open, + bodyshopid: bodyshop.id, + useremail: currentUser.email + }, + selectedJobLines + ); + notification.open({ + type: "success", + message: t("jobs.successes.ioucreated"), + onClick: () => history(`/manage/jobs/${iouId}`) + }); + const selectedJobLinesIds = selectedJobLines.map((l) => l.id); + await client.mutate({ + mutation: UPDATE_JOB_LINES_IOU, + variables: { ids: selectedJobLinesIds }, + update(cache) { + cache.modify({ + id: cache.identify(job.id), + fields: { + joblines(existingJobLines, { readField }) { + return existingJobLines.map((a) => { + if (!selectedJobLinesIds.includes(a.id)) return a; + return { ...a, ioucreated: true }; + }); + } + } + }); + } }); - if (IOU_Tracking.treatment !== "on") return null; + setLoading(false); + }; - const handleCreateIou = async () => { - setLoading(true); - //Query all of the job details to recreate. - const iouId = await CreateIouForJob( - client, - job.id, - { - status: bodyshop.md_ro_statuses.default_open, - bodyshopid: bodyshop.id, - useremail: currentUser.email, - }, - selectedJobLines - ); - notification.open({ - type: "success", - message: t("jobs.successes.ioucreated"), - onClick: () => history(`/manage/jobs/${iouId}`), - }); - const selectedJobLinesIds = selectedJobLines.map((l) => l.id); - await client.mutate({ - mutation: UPDATE_JOB_LINES_IOU, - variables: {ids: selectedJobLinesIds}, - update(cache) { - cache.modify({ - id: cache.identify(job.id), - fields: { - joblines(existingJobLines, {readField}) { - return existingJobLines.map((a) => { - if (!selectedJobLinesIds.includes(a.id)) return a; - return {...a, ioucreated: true}; - }); - }, - }, - }); - }, - }); - - setLoading(false); - }; - - return ( - - - {t("jobs.actions.createiou")} - - - ); + return ( + + + {t("jobs.actions.createiou")} + + + ); } diff --git a/client/src/components/job-damage-visual/job-damage-visual.component.jsx b/client/src/components/job-damage-visual/job-damage-visual.component.jsx index e6d2e3ce5..7efa6186d 100644 --- a/client/src/components/job-damage-visual/job-damage-visual.component.jsx +++ b/client/src/components/job-damage-visual/job-damage-visual.component.jsx @@ -1,748 +1,461 @@ import React from "react"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; -const Car = ({dmg1, dmg2}) => { - const {t} = useTranslation(); +const Car = ({ dmg1, dmg2 }) => { + const { t } = useTranslation(); - return ( - - {t("jobs.labels.cards.damage")} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- Click or drag files to this area to upload. -
+ +
Click or drag files to this area to upload.