Reformat all project files to use the prettier config file.

This commit is contained in:
Patrick Fic
2024-03-27 15:35:07 -07:00
parent b161530381
commit e1df64d592
873 changed files with 111387 additions and 125473 deletions

View File

@@ -1,16 +1,18 @@
exports.default = { const config = {
printWidth: 120, printWidth: 120,
useTabs: false, useTabs: false,
tabWidth: 2, tabWidth: 2,
trailingComma: 'es5', trailingComma: "none",
semi: true, semi: true,
singleQuote: false, singleQuote: false,
bracketSpacing: true, bracketSpacing: true,
arrowParens: 'always', arrowParens: "always",
jsxSingleQuote: false, jsxSingleQuote: false,
bracketSameLine: false, bracketSameLine: false,
endOfLine: 'lf', endOfLine: "lf",
importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'], importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true, importOrderSeparation: true,
importOrderSortSpecifiers: true, importOrderSortSpecifiers: true
}; };
module.exports = config;

View File

@@ -5,16 +5,16 @@ module.exports = {
cwd: "./io", cwd: "./io",
script: "./server.js", script: "./server.js",
env: { env: {
NODE_ENV: "test", NODE_ENV: "test"
}, }
}, },
{ {
name: "Bitbucket Webhook", name: "Bitbucket Webhook",
script: "./webhook/index.js", script: "./webhook/index.js",
env: { env: {
NODE_ENV: "production", NODE_ENV: "production"
}, }
}, }
], ]
}; };

View File

@@ -1,8 +1,8 @@
// craco.config.js // craco.config.js
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less"); const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib'); const { convertLegacyToken } = require("@ant-design/compatible/lib");
const {theme} = require('antd/lib'); const { theme } = require("antd/lib");
const { defaultAlgorithm, defaultSeed } = theme; const { defaultAlgorithm, defaultSeed } = theme;
@@ -13,18 +13,17 @@ const v4Token = convertLegacyToken(mapToken);
module.exports = { module.exports = {
plugins: [ plugins: [
{ {
plugin: CracoLessPlugin, plugin: CracoLessPlugin,
options: { options: {
lessLoaderOptions: { lessLoaderOptions: {
lessOptions: { lessOptions: {
modifyVars: { ...v4Token }, modifyVars: { ...v4Token },
javascriptEnabled: true, javascriptEnabled: true
}, }
}, }
}, }
}, }
], ],
webpack: { webpack: {
configure: (webpackConfig) => { configure: (webpackConfig) => {
@@ -33,7 +32,7 @@ module.exports = {
// Required for Dev Server // Required for Dev Server
devServer: { devServer: {
...webpackConfig.devServer, ...webpackConfig.devServer,
allowedHosts: 'all', allowedHosts: "all"
}, },
optimization: { optimization: {
...webpackConfig.optimization, ...webpackConfig.optimization,
@@ -45,10 +44,10 @@ module.exports = {
} }
return item; return item;
}), })
}, }
}; };
}
}, },
}, devtool: "source-map"
devtool: "source-map",
}; };

View File

@@ -1,17 +1,17 @@
const {defineConfig} = require('cypress') const { defineConfig } = require("cypress");
module.exports = defineConfig({ module.exports = defineConfig({
experimentalStudio: true, experimentalStudio: true,
env: { env: {
FIREBASE_USERNAME: 'cypress@imex.test', FIREBASE_USERNAME: "cypress@imex.test",
FIREBASE_PASSWORD: 'cypress', FIREBASE_PASSWORD: "cypress"
}, },
e2e: { e2e: {
// We've imported your old cypress plugins here. // We've imported your old cypress plugins here.
// You may want to clean this up later by importing these. // You may want to clean this up later by importing these.
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config) return require("./cypress/plugins/index.js")(on, config);
}, },
baseUrl: 'http://localhost:3000', baseUrl: "http://localhost:3000"
}, }
}) });

View File

@@ -4,16 +4,11 @@ describe("Renders the General Page", () => {
beforeEach(() => { beforeEach(() => {
cy.visit("/"); cy.visit("/");
}); });
it("Renders Correctly", () => { it("Renders Correctly", () => {});
});
it("Has the Slogan", () => { it("Has the Slogan", () => {
cy.findByText("A whole x22new kind of shop management system.").should( cy.findByText("A whole x22new kind of shop management system.").should("exist");
"exist"
);
/* ==== Generated with Cypress Studio ==== */ /* ==== Generated with Cypress Studio ==== */
cy.get( cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
).click();
cy.get("#email").clear(); cy.get("#email").clear();
cy.get("#email").type("patrick@imex.dev"); cy.get("#email").type("patrick@imex.dev");
cy.get("#password").clear(); cy.get("#password").clear();

View File

@@ -11,32 +11,32 @@
// please read our getting started guide: // please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress // https://on.cypress.io/introduction-to-cypress
describe('example to-do app', () => { describe("example to-do app", () => {
beforeEach(() => { beforeEach(() => {
// Cypress starts out with a blank slate for each test // Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command. // 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, // 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 // we include it in our beforeEach function so that it runs before each test
cy.visit('https://example.cypress.io/todo') cy.visit("https://example.cypress.io/todo");
}) });
it('displays two todo items by default', () => { it("displays two todo items by default", () => {
// We use the `cy.get()` command to get all elements that match the selector. // 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, // Then, we use `should` to assert that there are two matched items,
// which are the two default items. // which are the two default items.
cy.get('.todo-list li').should('have.length', 2) cy.get(".todo-list li").should("have.length", 2);
// We can go even further and check that the default todos each contain // We can go even further and check that the default todos each contain
// the correct text. We use the `first` and `last` functions // the correct text. We use the `first` and `last` functions
// to get just the first and last matched elements individually, // to get just the first and last matched elements individually,
// and then perform an assertion with `should`. // and then perform an assertion with `should`.
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
cy.get('.todo-list li').last().should('have.text', 'Walk the dog') cy.get(".todo-list li").last().should("have.text", "Walk the dog");
}) });
it('can add new todo items', () => { it("can add new todo items", () => {
// We'll store our item text in a variable so we can reuse it // We'll store our item text in a variable so we can reuse it
const newItem = 'Feed the cat' const newItem = "Feed the cat";
// Let's get the input element and use the `type` command to // Let's get the input element and use the `type` command to
// input our new list item. After typing the content of our item, // input our new list item. After typing the content of our item,
@@ -44,100 +44,81 @@ describe('example to-do app', () => {
// This input has a data-test attribute so we'll use that to select the // This input has a data-test attribute so we'll use that to select the
// element in accordance with best practices: // element in accordance with best practices:
// https://on.cypress.io/selecting-elements // https://on.cypress.io/selecting-elements
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) 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. // 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. // 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. // 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, // Since assertions yield the element that was asserted on,
// we can chain both of these assertions together into a single statement. // we can chain both of these assertions together into a single statement.
cy.get('.todo-list li') cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem);
.should('have.length', 3) });
.last()
.should('have.text', newItem)
})
it('can check off an item as completed', () => { it("can check off an item as completed", () => {
// In addition to using the `get` command to get an element by selector, // 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. // we can also use the `contains` command to get an element by its contents.
// However, this will yield the <label>, which is lowest-level element that contains the text. // However, this will yield the <label>, which is lowest-level element that contains the text.
// In order to check the item, we'll find the <input> element for this <label> // In order to check the item, we'll find the <input> element for this <label>
// by traversing up the dom to the parent element. From there, we can `find` // by traversing up the dom to the parent element. From there, we can `find`
// the child checkbox <input> element and use the `check` command to check it. // the child checkbox <input> element and use the `check` command to check it.
cy.contains('Pay electric bill') cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
.parent()
.find('input[type=checkbox]')
.check()
// Now that we've checked the button, we can go ahead and make sure // Now that we've checked the button, we can go ahead and make sure
// that the list element is now marked as completed. // that the list element is now marked as completed.
// Again we'll use `contains` to find the <label> element and then use the `parents` command // Again we'll use `contains` to find the <label> element and then use the `parents` command
// to traverse multiple levels up the dom until we find the corresponding <li> element. // to traverse multiple levels up the dom until we find the corresponding <li> element.
// Once we get that element, we can assert that it has the completed class. // Once we get that element, we can assert that it has the completed class.
cy.contains('Pay electric bill') cy.contains("Pay electric bill").parents("li").should("have.class", "completed");
.parents('li') });
.should('have.class', 'completed')
})
context('with a checked task', () => { context("with a checked task", () => {
beforeEach(() => { beforeEach(() => {
// We'll take the command we used above to check off an element // We'll take the command we used above to check off an element
// Since we want to perform multiple tests that start with checking // Since we want to perform multiple tests that start with checking
// one element, we put it in the beforeEach hook // one element, we put it in the beforeEach hook
// so that it runs at the start of every test. // so that it runs at the start of every test.
cy.contains('Pay electric bill') cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
.parent() });
.find('input[type=checkbox]')
.check()
})
it('can filter for uncompleted tasks', () => { it("can filter for uncompleted tasks", () => {
// We'll click on the "active" button in order to // We'll click on the "active" button in order to
// display only incomplete items // display only incomplete items
cy.contains('Active').click() cy.contains("Active").click();
// After filtering, we can assert that there is only the one // After filtering, we can assert that there is only the one
// incomplete item in the list. // incomplete item in the list.
cy.get('.todo-list li') cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog");
.should('have.length', 1)
.first()
.should('have.text', 'Walk the dog')
// For good measure, let's also assert that the task we checked off // For good measure, let's also assert that the task we checked off
// does not exist on the page. // does not exist on the page.
cy.contains('Pay electric bill').should('not.exist') cy.contains("Pay electric bill").should("not.exist");
}) });
it('can filter for completed tasks', () => { it("can filter for completed tasks", () => {
// We can perform similar steps as the test above to ensure // We can perform similar steps as the test above to ensure
// that only completed tasks are shown // that only completed tasks are shown
cy.contains('Completed').click() cy.contains("Completed").click();
cy.get('.todo-list li') cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill");
.should('have.length', 1)
.first()
.should('have.text', 'Pay electric bill')
cy.contains('Walk the dog').should('not.exist') cy.contains("Walk the dog").should("not.exist");
}) });
it('can delete all completed tasks', () => { it("can delete all completed tasks", () => {
// First, let's click the "Clear completed" button // First, let's click the "Clear completed" button
// `contains` is actually serving two purposes here. // `contains` is actually serving two purposes here.
// First, it's ensuring that the button exists within the dom. // First, it's ensuring that the button exists within the dom.
// This button only appears when at least one task is checked // This button only appears when at least one task is checked
// so this command is implicitly verifying that it does exist. // so this command is implicitly verifying that it does exist.
// Second, it selects the button so we can click it. // Second, it selects the button so we can click it.
cy.contains('Clear completed').click() cy.contains("Clear completed").click();
// Then we can make sure that there is only one element // Then we can make sure that there is only one element
// in the list and our element does not exist // in the list and our element does not exist
cy.get('.todo-list li') cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill");
.should('have.length', 1)
.should('not.have.text', 'Pay electric bill')
// Finally, make sure that the clear button no longer exists. // Finally, make sure that the clear button no longer exists.
cy.contains('Clear completed').should('not.exist') cy.contains("Clear completed").should("not.exist");
}) });
}) });
}) });

View File

@@ -1,72 +1,73 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Actions', () => { context("Actions", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions') 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', () => { it(".type() - type into a DOM element", () => {
// https://on.cypress.io/type // https://on.cypress.io/type
cy.get('.action-email') cy.get(".action-email")
.type('fake@email.com').should('have.value', 'fake@email.com') .type("fake@email.com")
.should("have.value", "fake@email.com")
// .type() with special character sequences // .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}') .type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
.type('{del}{selectall}{backspace}') .type("{del}{selectall}{backspace}")
// .type() with key modifiers // .type() with key modifiers
.type('{alt}{option}') //these are equivalent .type("{alt}{option}") //these are equivalent
.type('{ctrl}{control}') //these are equivalent .type("{ctrl}{control}") //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent .type("{meta}{command}{cmd}") //these are equivalent
.type('{shift}') .type("{shift}")
// Delay each keypress by 0.1 sec // Delay each keypress by 0.1 sec
.type('slow.typing@email.com', {delay: 100}) .type("slow.typing@email.com", { delay: 100 })
.should('have.value', 'slow.typing@email.com') .should("have.value", "slow.typing@email.com");
cy.get('.action-disabled') cy.get(".action-disabled")
// Ignore error checking prior to type // Ignore error checking prior to type
// like whether the input is visible or disabled // like whether the input is visible or disabled
.type('disabled error checking', {force: true}) .type("disabled error checking", { force: true })
.should('have.value', 'disabled error checking') .should("have.value", "disabled error checking");
}) });
it('.focus() - focus on a DOM element', () => { it(".focus() - focus on a DOM element", () => {
// https://on.cypress.io/focus // https://on.cypress.io/focus
cy.get('.action-focus').focus() cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;");
.should('have.class', 'focus') });
.prev().should('have.attr', 'style', 'color: orange;')
})
it('.blur() - blur off a DOM element', () => { it(".blur() - blur off a DOM element", () => {
// https://on.cypress.io/blur // https://on.cypress.io/blur
cy.get('.action-blur').type('About to blur').blur() cy.get(".action-blur")
.should('have.class', 'error') .type("About to blur")
.prev().should('have.attr', 'style', 'color: red;') .blur()
}) .should("have.class", "error")
.prev()
.should("have.attr", "style", "color: red;");
});
it('.clear() - clears an input or textarea element', () => { it(".clear() - clears an input or textarea element", () => {
// https://on.cypress.io/clear // https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text') cy.get(".action-clear")
.should('have.value', 'Clear this text') .type("Clear this text")
.should("have.value", "Clear this text")
.clear() .clear()
.should('have.value', '') .should("have.value", "");
}) });
it('.submit() - submit a form', () => { it(".submit() - submit a form", () => {
// https://on.cypress.io/submit // https://on.cypress.io/submit
cy.get('.action-form') cy.get(".action-form").find('[type="text"]').type("HALFOFF");
.find('[type="text"]').type('HALFOFF')
cy.get('.action-form').submit() cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
.next().should('contain', 'Your form has been submitted!') });
})
it('.click() - click on a DOM element', () => { it(".click() - click on a DOM element", () => {
// https://on.cypress.io/click // https://on.cypress.io/click
cy.get('.action-btn').click() cy.get(".action-btn").click();
// You can click on 9 specific positions of an element: // You can click on 9 specific positions of an element:
// ----------------------------------- // -----------------------------------
@@ -82,169 +83,152 @@ context('Actions', () => {
// ----------------------------------- // -----------------------------------
// clicking in the center of the element is the default // clicking in the center of the element is the default
cy.get('#action-canvas').click() cy.get("#action-canvas").click();
cy.get('#action-canvas').click('topLeft') cy.get("#action-canvas").click("topLeft");
cy.get('#action-canvas').click('top') cy.get("#action-canvas").click("top");
cy.get('#action-canvas').click('topRight') cy.get("#action-canvas").click("topRight");
cy.get('#action-canvas').click('left') cy.get("#action-canvas").click("left");
cy.get('#action-canvas').click('right') cy.get("#action-canvas").click("right");
cy.get('#action-canvas').click('bottomLeft') cy.get("#action-canvas").click("bottomLeft");
cy.get('#action-canvas').click('bottom') cy.get("#action-canvas").click("bottom");
cy.get('#action-canvas').click('bottomRight') cy.get("#action-canvas").click("bottomRight");
// .click() accepts an x and y coordinate // .click() accepts an x and y coordinate
// that controls where the click occurs :) // that controls where the click occurs :)
cy.get('#action-canvas') cy.get("#action-canvas")
.click(80, 75) // click 80px on x coord and 75px on y coord .click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75) .click(170, 75)
.click(80, 165) .click(80, 165)
.click(100, 185) .click(100, 185)
.click(125, 190) .click(125, 190)
.click(150, 185) .click(150, 185)
.click(170, 165) .click(170, 165);
// click multiple elements by passing multiple: true // click multiple elements by passing multiple: true
cy.get('.action-labels>.label').click({multiple: true}) cy.get(".action-labels>.label").click({ multiple: true });
// Ignore error checking prior to clicking // Ignore error checking prior to clicking
cy.get('.action-opacity>.btn').click({force: true}) cy.get(".action-opacity>.btn").click({ force: true });
}) });
it('.dblclick() - double click on a DOM element', () => { it(".dblclick() - double click on a DOM element", () => {
// https://on.cypress.io/dblclick // https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js' // Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click // that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible') cy.get(".action-div").dblclick().should("not.be.visible");
cy.get('.action-input-hidden').should('be.visible') cy.get(".action-input-hidden").should("be.visible");
}) });
it('.rightclick() - right click on a DOM element', () => { it(".rightclick() - right click on a DOM element", () => {
// https://on.cypress.io/rightclick // https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js' // Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click // 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-div").rightclick().should("not.be.visible");
cy.get('.rightclick-action-input-hidden').should('be.visible') cy.get(".rightclick-action-input-hidden").should("be.visible");
}) });
it('.check() - check a checkbox or radio element', () => { it(".check() - check a checkbox or radio element", () => {
// https://on.cypress.io/check // https://on.cypress.io/check
// By default, .check() will check all // By default, .check() will check all
// matching checkbox or radio elements in succession, one after another // matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not('[disabled]') cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
.check().should('be.checked')
// .check() accepts a value argument // .check() accepts a value argument
cy.get('.action-radios [type="radio"]') cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
.check('radio1').should('be.checked')
// .check() accepts an array of values // .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]') cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
.check(['checkbox1', 'checkbox2']).should('be.checked')
// Ignore error checking prior to checking // Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]') cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
.check({force: true}).should('be.checked')
cy.get('.action-radios [type="radio"]') cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
.check('radio3', {force: true}).should('be.checked') });
})
it('.uncheck() - uncheck a checkbox element', () => { it(".uncheck() - uncheck a checkbox element", () => {
// https://on.cypress.io/uncheck // https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching // By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another // checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
.not('[disabled]')
.uncheck().should('not.be.checked')
// .uncheck() accepts a value argument // .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
// .uncheck() accepts an array of values // .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]') cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3']) .check(["checkbox1", "checkbox3"])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') .uncheck(["checkbox1", "checkbox3"])
.should("not.be.checked");
// Ignore error checking prior to unchecking // Ignore error checking prior to unchecking
cy.get('.action-check [disabled]') cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked");
.uncheck({force: true}).should('not.be.checked') });
})
it('.select() - select an option in a <select> element', () => { it(".select() - select an option in a <select> element", () => {
// https://on.cypress.io/select // https://on.cypress.io/select
// at first, no option should be selected // at first, no option should be selected
cy.get('.action-select') cy.get(".action-select").should("have.value", "--Select a fruit--");
.should('have.value', '--Select a fruit--')
// Select option(s) with matching text content // Select option(s) with matching text content
cy.get('.action-select').select('apples') cy.get(".action-select").select("apples");
// confirm the apples were selected // confirm the apples were selected
// note that each value starts with "fr-" in our HTML // note that each value starts with "fr-" in our HTML
cy.get('.action-select').should('have.value', 'fr-apples') cy.get(".action-select").should("have.value", "fr-apples");
cy.get('.action-select-multiple') cy.get(".action-select-multiple")
.select(['apples', 'oranges', 'bananas']) .select(["apples", "oranges", "bananas"])
// when getting multiple values, invoke "val" method first // when getting multiple values, invoke "val" method first
.invoke('val') .invoke("val")
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// Select option(s) with matching value // Select option(s) with matching value
cy.get('.action-select').select('fr-bananas') cy.get(".action-select")
.select("fr-bananas")
// can attach an assertion right away to the element // can attach an assertion right away to the element
.should('have.value', 'fr-bananas') .should("have.value", "fr-bananas");
cy.get('.action-select-multiple') cy.get(".action-select-multiple")
.select(['fr-apples', 'fr-oranges', 'fr-bananas']) .select(["fr-apples", "fr-oranges", "fr-bananas"])
.invoke('val') .invoke("val")
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// assert the selected values include oranges // assert the selected values include oranges
cy.get('.action-select-multiple') cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges");
.invoke('val').should('include', 'fr-oranges') });
})
it('.scrollIntoView() - scroll an element into view', () => { it(".scrollIntoView() - scroll an element into view", () => {
// https://on.cypress.io/scrollintoview // https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden, // normally all of these buttons are hidden,
// because they're not within // because they're not within
// the viewable area of their parent // the viewable area of their parent
// (we need to scroll to see them) // (we need to scroll to see them)
cy.get('#scroll-horizontal button') cy.get("#scroll-horizontal button").should("not.be.visible");
.should('not.be.visible')
// scroll the button into view, as if the user had scrolled // scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView() cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
.should('be.visible')
cy.get('#scroll-vertical button') cy.get("#scroll-vertical button").should("not.be.visible");
.should('not.be.visible')
// Cypress handles the scroll direction needed // Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView() cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
.should('be.visible')
cy.get('#scroll-both button') cy.get("#scroll-both button").should("not.be.visible");
.should('not.be.visible')
// Cypress knows to scroll to the right and down // Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView() cy.get("#scroll-both button").scrollIntoView().should("be.visible");
.should('be.visible') });
})
it('.trigger() - trigger an event on a DOM element', () => { it(".trigger() - trigger an event on a DOM element", () => {
// https://on.cypress.io/trigger // https://on.cypress.io/trigger
// To interact with a range input (slider) // To interact with a range input (slider)
@@ -253,14 +237,15 @@ context('Actions', () => {
// Here, we invoke jQuery's val() method to set // Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event // the value and trigger the 'change' event
cy.get('.trigger-input-range') cy.get(".trigger-input-range")
.invoke('val', 25) .invoke("val", 25)
.trigger('change') .trigger("change")
.get('input[type=range]').siblings('p') .get("input[type=range]")
.should('have.text', '25') .siblings("p")
}) .should("have.text", "25");
});
it('cy.scrollTo() - scroll the window or element to a position', () => { it("cy.scrollTo() - scroll the window or element to a position", () => {
// https://on.cypress.io/scrollto // https://on.cypress.io/scrollto
// You can scroll to 9 specific positions of an element: // You can scroll to 9 specific positions of an element:
@@ -278,22 +263,22 @@ context('Actions', () => {
// if you chain .scrollTo() off of cy, we will // if you chain .scrollTo() off of cy, we will
// scroll the entire window // scroll the entire window
cy.scrollTo('bottom') cy.scrollTo("bottom");
cy.get('#scrollable-horizontal').scrollTo('right') cy.get("#scrollable-horizontal").scrollTo("right");
// or you can scroll to a specific coordinate: // or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels // (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250) cy.get("#scrollable-vertical").scrollTo(250, 250);
// or you can scroll to a specific percentage // or you can scroll to a specific percentage
// of the (width, height) of the element // of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%') cy.get("#scrollable-both").scrollTo("75%", "25%");
// control the easing of the scroll (default is 'swing') // control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', {easing: 'linear'}) cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
// control the duration of the scroll (in ms) // control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', {duration: 2000}) cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
}) });
}) });

View File

@@ -1,39 +1,35 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Aliasing', () => { context("Aliasing", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/aliasing') cy.visit("https://example.cypress.io/commands/aliasing");
}) });
it('.as() - alias a DOM element for later use', () => { it(".as() - alias a DOM element for later use", () => {
// https://on.cypress.io/as // https://on.cypress.io/as
// Alias a DOM element for use later // Alias a DOM element for use later
// We don't have to traverse to the element // We don't have to traverse to the element
// later in our code, we reference it with @ // later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr') cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
.first().find('td').first()
.find('button').as('firstBtn')
// when we reference the alias, we place an // when we reference the alias, we place an
// @ in front of its name // @ in front of its name
cy.get('@firstBtn').click() cy.get("@firstBtn").click();
cy.get('@firstBtn') cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
.should('have.class', 'btn-success') });
.and('contain', 'Changed')
})
it('.as() - alias a route for later use', () => { it(".as() - alias a route for later use", () => {
// Alias the route to wait for its response // Alias the route to wait for its response
cy.intercept('GET', '**/comments/*').as('getComment') cy.intercept("GET", "**/comments/*").as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-btn').click() cy.get(".network-btn").click();
// https://on.cypress.io/wait // https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('eq', 200) cy.wait("@getComment").its("response.statusCode").should("eq", 200);
}) });
}) });

View File

@@ -1,177 +1,173 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Assertions', () => { context("Assertions", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions') cy.visit("https://example.cypress.io/commands/assertions");
}) });
describe('Implicit Assertions', () => { describe("Implicit Assertions", () => {
it('.should() - make an assertion about the current subject', () => { it(".should() - make an assertion about the current subject", () => {
// https://on.cypress.io/should // https://on.cypress.io/should
cy.get('.assertion-table') cy.get(".assertion-table")
.find('tbody tr:last') .find("tbody tr:last")
.should('have.class', 'success') .should("have.class", "success")
.find('td') .find("td")
.first() .first()
// checking the text of the <td> element in various ways // checking the text of the <td> element in various ways
.should('have.text', 'Column content') .should("have.text", "Column content")
.should('contain', 'Column content') .should("contain", "Column content")
.should('have.html', 'Column content') .should("have.html", "Column content")
// chai-jquery uses "is()" to check if element matches selector // chai-jquery uses "is()" to check if element matches selector
.should('match', 'td') .should("match", "td")
// to match text content against a regular expression // to match text content against a regular expression
// first need to invoke jQuery method text() // first need to invoke jQuery method text()
// and then match using regular expression // and then match using regular expression
.invoke('text') .invoke("text")
.should('match', /column content/i) .should("match", /column content/i);
// a better way to check element's text content against a regular expression // a better way to check element's text content against a regular expression
// is to use "cy.contains" // is to use "cy.contains"
// https://on.cypress.io/contains // https://on.cypress.io/contains
cy.get('.assertion-table') cy.get(".assertion-table")
.find('tbody tr:last') .find("tbody tr:last")
// finds first <td> element with text content matching regular expression // finds first <td> element with text content matching regular expression
.contains('td', /column content/i) .contains("td", /column content/i)
.should('be.visible') .should("be.visible");
// for more information about asserting element's text // for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
}) });
it('.and() - chain multiple assertions together', () => { it(".and() - chain multiple assertions together", () => {
// https://on.cypress.io/and // https://on.cypress.io/and
cy.get('.assertions-link') cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
.should('have.class', 'active') });
.and('have.attr', 'href') });
.and('include', 'cypress.io')
})
})
describe('Explicit Assertions', () => { describe("Explicit Assertions", () => {
// https://on.cypress.io/assertions // https://on.cypress.io/assertions
it('expect - make an assertion about a specified subject', () => { it("expect - make an assertion about a specified subject", () => {
// We can use Chai's BDD style assertions // We can use Chai's BDD style assertions
expect(true).to.be.true expect(true).to.be.true;
const o = {foo: 'bar'} const o = { foo: "bar" };
expect(o).to.equal(o) expect(o).to.equal(o);
expect(o).to.deep.equal({foo: 'bar'}) expect(o).to.deep.equal({ foo: "bar" });
// matching text using regular expression // matching text using regular expression
expect('FooBar').to.match(/bar$/i) expect("FooBar").to.match(/bar$/i);
}) });
it('pass your own callback function to should()', () => { it("pass your own callback function to should()", () => {
// Pass a function to should that can have any number // Pass a function to should that can have any number
// of explicit assertions within it. // of explicit assertions within it.
// The ".should(cb)" function will be retried // The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out. // automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p') cy.get(".assertions-p")
.find('p') .find("p")
.should(($p) => { .should(($p) => {
// https://on.cypress.io/$ // https://on.cypress.io/$
// return an array of texts from all of the p's // return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable // @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text()) const texts = $p.map((i, el) => Cypress.$(el).text());
// jquery map returns jquery object // jquery map returns jquery object
// and .get() convert this to simple array // and .get() convert this to simple array
const paragraphs = texts.get() const paragraphs = texts.get();
// array should have length of 3 // array should have length of 3
expect(paragraphs, 'has 3 paragraphs').to.have.length(3) expect(paragraphs, "has 3 paragraphs").to.have.length(3);
// use second argument to expect(...) to provide clear // use second argument to expect(...) to provide clear
// message with each assertion // message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
'Some text from first p', "Some text from first p",
'More text from second p', "More text from second p",
'And even more text from third p', "And even more text from third p"
]) ]);
}) });
}) });
it('finds element by class name regex', () => { it("finds element by class name regex", () => {
cy.get('.docs-header') cy.get(".docs-header")
.find('div') .find("div")
// .should(cb) callback function will be retried // .should(cb) callback function will be retried
.should(($div) => { .should(($div) => {
expect($div).to.have.length(1) expect($div).to.have.length(1);
const className = $div[0].className const className = $div[0].className;
expect(className).to.match(/heading-/) expect(className).to.match(/heading-/);
}) })
// .then(cb) callback is not retried, // .then(cb) callback is not retried,
// it either passes or fails // it either passes or fails
.then(($div) => { .then(($div) => {
expect($div, 'text content').to.have.text('Introduction') expect($div, "text content").to.have.text("Introduction");
}) });
}) });
it('can throw any error', () => { it("can throw any error", () => {
cy.get('.docs-header') cy.get(".docs-header")
.find('div') .find("div")
.should(($div) => { .should(($div) => {
if ($div.length !== 1) { if ($div.length !== 1) {
// you can throw your own errors // you can throw your own errors
throw new Error('Did not find 1 element') throw new Error("Did not find 1 element");
} }
const className = $div[0].className const className = $div[0].className;
if (!className.match(/heading-/)) { if (!className.match(/heading-/)) {
throw new Error(`Could not find class "heading-" in ${className}`) throw new Error(`Could not find class "heading-" in ${className}`);
} }
}) });
}) });
it('matches unknown text between two elements', () => { it("matches unknown text between two elements", () => {
/** /**
* Text from the first element. * Text from the first element.
* @type {string} * @type {string}
*/ */
let text let text;
/** /**
* Normalizes passed text, * Normalizes passed text,
* useful before comparing text with spaces and different capitalization. * useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize * @param {string} s Text to normalize
*/ */
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
cy.get('.two-elements') cy.get(".two-elements")
.find('.first') .find(".first")
.then(($first) => { .then(($first) => {
// save text from the first element // save text from the first element
text = normalizeText($first.text()) text = normalizeText($first.text());
}) });
cy.get('.two-elements') cy.get(".two-elements")
.find('.second') .find(".second")
.should(($div) => { .should(($div) => {
// we can massage text before comparing // we can massage text before comparing
const secondText = normalizeText($div.text()) const secondText = normalizeText($div.text());
expect(secondText, 'second text').to.equal(text) expect(secondText, "second text").to.equal(text);
}) });
}) });
it('assert - assert shape of an object', () => { it("assert - assert shape of an object", () => {
const person = { const person = {
name: 'Joe', name: "Joe",
age: 20, age: 20
} };
assert.isObject(person, 'value is object') assert.isObject(person, "value is object");
}) });
it('retries the should callback until assertions pass', () => { it("retries the should callback until assertions pass", () => {
cy.get('#random-number') cy.get("#random-number").should(($div) => {
.should(($div) => { const n = parseFloat($div.text());
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);
}) });
}) });
}) });
}) });

View File

@@ -1,97 +1,96 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Connectors', () => { context("Connectors", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/connectors') cy.visit("https://example.cypress.io/commands/connectors");
}) });
it('.each() - iterate over an array of elements', () => { it(".each() - iterate over an array of elements", () => {
// https://on.cypress.io/each // https://on.cypress.io/each
cy.get('.connectors-each-ul>li') cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
.each(($el, index, $list) => { console.log($el, index, $list);
console.log($el, index, $list) });
}) });
})
it('.its() - get properties on the current subject', () => { it(".its() - get properties on the current subject", () => {
// https://on.cypress.io/its // https://on.cypress.io/its
cy.get('.connectors-its-ul>li') cy.get(".connectors-its-ul>li")
// calls the 'length' property yielding that value // calls the 'length' property yielding that value
.its('length') .its("length")
.should('be.gt', 2) .should("be.gt", 2);
}) });
it('.invoke() - invoke a function on the current subject', () => { it(".invoke() - invoke a function on the current subject", () => {
// our div is hidden in our script.js // our div is hidden in our script.js
// $('.connectors-div').hide() // $('.connectors-div').hide()
// https://on.cypress.io/invoke // https://on.cypress.io/invoke
cy.get('.connectors-div').should('be.hidden') cy.get(".connectors-div")
.should("be.hidden")
// call the jquery method 'show' on the 'div.container' // call the jquery method 'show' on the 'div.container'
.invoke('show') .invoke("show")
.should('be.visible') .should("be.visible");
}) });
it('.spread() - spread an array as individual args to callback function', () => { it(".spread() - spread an array as individual args to callback function", () => {
// https://on.cypress.io/spread // https://on.cypress.io/spread
const arr = ['foo', 'bar', 'baz'] const arr = ["foo", "bar", "baz"];
cy.wrap(arr).spread((foo, bar, baz) => { cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo') expect(foo).to.eq("foo");
expect(bar).to.eq('bar') expect(bar).to.eq("bar");
expect(baz).to.eq('baz') expect(baz).to.eq("baz");
}) });
}) });
describe('.then()', () => { describe(".then()", () => {
it('invokes a callback function with the current subject', () => { it("invokes a callback function with the current subject", () => {
// https://on.cypress.io/then // https://on.cypress.io/then
cy.get('.connectors-list > li') cy.get(".connectors-list > li").then(($lis) => {
.then(($lis) => { expect($lis, "3 items").to.have.length(3);
expect($lis, '3 items').to.have.length(3) expect($lis.eq(0), "first item").to.contain("Walk the dog");
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(1), 'second item').to.contain('Feed the cat') expect($lis.eq(2), "third item").to.contain("Write JavaScript");
expect($lis.eq(2), 'third item').to.contain('Write JavaScript') });
}) });
})
it('yields the returned value to the next command', () => { it("yields the returned value to the next command", () => {
cy.wrap(1) cy.wrap(1)
.then((num) => { .then((num) => {
expect(num).to.equal(1) expect(num).to.equal(1);
return 2 return 2;
}) })
.then((num) => { .then((num) => {
expect(num).to.equal(2) expect(num).to.equal(2);
}) });
}) });
it('yields the original subject without return', () => { it("yields the original subject without return", () => {
cy.wrap(1) cy.wrap(1)
.then((num) => { .then((num) => {
expect(num).to.equal(1) expect(num).to.equal(1);
// note that nothing is returned from this callback // note that nothing is returned from this callback
}) })
.then((num) => { .then((num) => {
// this callback receives the original unchanged value 1 // this callback receives the original unchanged value 1
expect(num).to.equal(1) expect(num).to.equal(1);
}) });
}) });
it('yields the value yielded by the last Cypress command inside', () => { it("yields the value yielded by the last Cypress command inside", () => {
cy.wrap(1) cy.wrap(1)
.then((num) => { .then((num) => {
expect(num).to.equal(1) expect(num).to.equal(1);
// note how we run a Cypress command // note how we run a Cypress command
// the result yielded by this Cypress command // the result yielded by this Cypress command
// will be passed to the second ".then" // will be passed to the second ".then"
cy.wrap(2) cy.wrap(2);
}) })
.then((num) => { .then((num) => {
// this callback receives the value yielded by "cy.wrap(2)" // this callback receives the value yielded by "cy.wrap(2)"
expect(num).to.equal(2) expect(num).to.equal(2);
}) });
}) });
}) });
}) });

View File

@@ -1,77 +1,79 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Cookies', () => { context("Cookies", () => {
beforeEach(() => { beforeEach(() => {
Cypress.Cookies.debug(true) 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 // clear cookies again after visiting to remove
// any 3rd party cookies picked up such as cloudflare // any 3rd party cookies picked up such as cloudflare
cy.clearCookies() cy.clearCookies();
}) });
it('cy.getCookie() - get a browser cookie', () => { it("cy.getCookie() - get a browser cookie", () => {
// https://on.cypress.io/getcookie // https://on.cypress.io/getcookie
cy.get('#getCookie .set-a-cookie').click() cy.get("#getCookie .set-a-cookie").click();
// cy.getCookie() yields a cookie object // cy.getCookie() yields a cookie object
cy.getCookie('token').should('have.property', 'value', '123ABC') cy.getCookie("token").should("have.property", "value", "123ABC");
}) });
it('cy.getCookies() - get browser cookies', () => { it("cy.getCookies() - get browser cookies", () => {
// https://on.cypress.io/getcookies // https://on.cypress.io/getcookies
cy.getCookies().should('be.empty') 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() yields an array of cookies
cy.getCookies().should('have.length', 1).should((cookies) => { cy.getCookies()
.should("have.length", 1)
.should((cookies) => {
// each cookie has these properties // each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token') expect(cookies[0]).to.have.property("name", "token");
expect(cookies[0]).to.have.property('value', '123ABC') expect(cookies[0]).to.have.property("value", "123ABC");
expect(cookies[0]).to.have.property('httpOnly', false) expect(cookies[0]).to.have.property("httpOnly", false);
expect(cookies[0]).to.have.property('secure', false) expect(cookies[0]).to.have.property("secure", false);
expect(cookies[0]).to.have.property('domain') expect(cookies[0]).to.have.property("domain");
expect(cookies[0]).to.have.property('path') expect(cookies[0]).to.have.property("path");
}) });
}) });
it('cy.setCookie() - set a browser cookie', () => { it("cy.setCookie() - set a browser cookie", () => {
// https://on.cypress.io/setcookie // https://on.cypress.io/setcookie
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
cy.setCookie('foo', 'bar') cy.setCookie("foo", "bar");
// cy.getCookie() yields a cookie object // cy.getCookie() yields a cookie object
cy.getCookie('foo').should('have.property', 'value', 'bar') cy.getCookie("foo").should("have.property", "value", "bar");
}) });
it('cy.clearCookie() - clear a browser cookie', () => { it("cy.clearCookie() - clear a browser cookie", () => {
// https://on.cypress.io/clearcookie // https://on.cypress.io/clearcookie
cy.getCookie('token').should('be.null') 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.clearCookies() yields null
cy.clearCookie('token').should('be.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', () => { it("cy.clearCookies() - clear browser cookies", () => {
// https://on.cypress.io/clearcookies // https://on.cypress.io/clearcookies
cy.getCookies().should('be.empty') 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() yields null
cy.clearCookies() cy.clearCookies();
cy.getCookies().should('be.empty') cy.getCookies().should("be.empty");
}) });
}) });

View File

@@ -1,202 +1,208 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Cypress.Commands', () => { context("Cypress.Commands", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') 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', () => { it(".add() - create a custom command", () => {
Cypress.Commands.add('console', { Cypress.Commands.add(
prevSubject: true, "console",
}, (subject, method) => { {
prevSubject: true
},
(subject, method) => {
// the previous subject is automatically received // the previous subject is automatically received
// and the commands arguments are shifted // and the commands arguments are shifted
// allow us to change the console method used // allow us to change the console method used
method = method || 'log' method = method || "log";
// log the subject to the console // log the subject to the console
// @ts-ignore TS7017 // @ts-ignore TS7017
console[method]('The subject is', subject) console[method]("The subject is", subject);
// whatever we return becomes the new subject // whatever we return becomes the new subject
// we don't want to change the subject so // we don't want to change the subject so
// we return whatever was passed in // we return whatever was passed in
return subject return subject;
}) }
);
// @ts-ignore TS2339 // @ts-ignore TS2339
cy.get('button').console('info').then(($button) => { cy.get("button")
.console("info")
.then(($button) => {
// subject is still $button // subject is still $button
}) });
}) });
}) });
context('Cypress.Cookies', () => { context("Cypress.Cookies", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// https://on.cypress.io/cookies // https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => { it(".debug() - enable or disable debugging", () => {
Cypress.Cookies.debug(true) Cypress.Cookies.debug(true);
// Cypress will now log in the console when // Cypress will now log in the console when
// cookies are set or cleared // cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie('fakeCookie') cy.clearCookie("fakeCookie");
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie('fakeCookie') cy.clearCookie("fakeCookie");
cy.setCookie('fakeCookie', '123ABC') cy.setCookie("fakeCookie", "123ABC");
}) });
it('.preserveOnce() - preserve cookies by key', () => { it(".preserveOnce() - preserve cookies by key", () => {
// normally cookies are reset after each test // normally cookies are reset after each test
cy.getCookie('fakeCookie').should('not.be.ok') cy.getCookie("fakeCookie").should("not.be.ok");
// preserving a cookie will not clear it when // preserving a cookie will not clear it when
// the next test starts // the next test starts
cy.setCookie('lastCookie', '789XYZ') cy.setCookie("lastCookie", "789XYZ");
Cypress.Cookies.preserveOnce('lastCookie') Cypress.Cookies.preserveOnce("lastCookie");
}) });
it('.defaults() - set defaults for all cookies', () => { it(".defaults() - set defaults for all cookies", () => {
// now any cookie with the name 'session_id' will // now any cookie with the name 'session_id' will
// not be cleared before each new test runs // not be cleared before each new test runs
Cypress.Cookies.defaults({ Cypress.Cookies.defaults({
preserve: 'session_id', preserve: "session_id"
}) });
}) });
}) });
context('Cypress.arch', () => { context("Cypress.arch", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get CPU architecture name of underlying OS', () => { it("Get CPU architecture name of underlying OS", () => {
// https://on.cypress.io/arch // https://on.cypress.io/arch
expect(Cypress.arch).to.exist expect(Cypress.arch).to.exist;
}) });
}) });
context('Cypress.config()', () => { context("Cypress.config()", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get and set configuration options', () => { it("Get and set configuration options", () => {
// https://on.cypress.io/config // https://on.cypress.io/config
let myConfig = Cypress.config() let myConfig = Cypress.config();
expect(myConfig).to.have.property('animationDistanceThreshold', 5) expect(myConfig).to.have.property("animationDistanceThreshold", 5);
expect(myConfig).to.have.property('baseUrl', null) expect(myConfig).to.have.property("baseUrl", null);
expect(myConfig).to.have.property('defaultCommandTimeout', 4000) expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
expect(myConfig).to.have.property('requestTimeout', 5000) expect(myConfig).to.have.property("requestTimeout", 5000);
expect(myConfig).to.have.property('responseTimeout', 30000) expect(myConfig).to.have.property("responseTimeout", 30000);
expect(myConfig).to.have.property('viewportHeight', 660) expect(myConfig).to.have.property("viewportHeight", 660);
expect(myConfig).to.have.property('viewportWidth', 1000) expect(myConfig).to.have.property("viewportWidth", 1000);
expect(myConfig).to.have.property('pageLoadTimeout', 60000) expect(myConfig).to.have.property("pageLoadTimeout", 60000);
expect(myConfig).to.have.property('waitForAnimations', true) 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! // this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000) 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', () => { context("Cypress.dom", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
// https://on.cypress.io/dom // https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => { it(".isHidden() - determine if a DOM element is hidden", () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0) let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
let visibleP = Cypress.$('.dom-p p.visible').get(0) let visibleP = Cypress.$(".dom-p p.visible").get(0);
// our first paragraph has css class 'hidden' // our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
expect(Cypress.dom.isHidden(visibleP)).to.be.false expect(Cypress.dom.isHidden(visibleP)).to.be.false;
}) });
}) });
context('Cypress.env()', () => { context("Cypress.env()", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') 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 // https://on.cypress.io/environment-variables
it('Get environment variables', () => { it("Get environment variables", () => {
// https://on.cypress.io/env // https://on.cypress.io/env
// set multiple environment variables // set multiple environment variables
Cypress.env({ Cypress.env({
host: 'veronica.dev.local', host: "veronica.dev.local",
api_server: 'http://localhost:8888/v1/', api_server: "http://localhost:8888/v1/"
}) });
// get environment variable // get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local') expect(Cypress.env("host")).to.eq("veronica.dev.local");
// set environment variable // set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/') Cypress.env("api_server", "http://localhost:8888/v2/");
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
// get all environment variable // get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
}) });
}) });
context('Cypress.log', () => { context("Cypress.log", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Control what is printed to the Command Log', () => { it("Control what is printed to the Command Log", () => {
// https://on.cypress.io/cypress-log // https://on.cypress.io/cypress-log
}) });
}) });
context('Cypress.platform', () => { context("Cypress.platform", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get underlying OS name', () => { it("Get underlying OS name", () => {
// https://on.cypress.io/platform // https://on.cypress.io/platform
expect(Cypress.platform).to.be.exist expect(Cypress.platform).to.be.exist;
}) });
}) });
context('Cypress.version', () => { context("Cypress.version", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get current version of Cypress being run', () => { it("Get current version of Cypress being run", () => {
// https://on.cypress.io/version // https://on.cypress.io/version
expect(Cypress.version).to.be.exist expect(Cypress.version).to.be.exist;
}) });
}) });
context('Cypress.spec', () => { context("Cypress.spec", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api') cy.visit("https://example.cypress.io/cypress-api");
}) });
it('Get current spec information', () => { it("Get current spec information", () => {
// https://on.cypress.io/spec // https://on.cypress.io/spec
// wrap the object so we can inspect it easily by clicking in the command log // 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']) cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]);
}) });
}) });

View File

@@ -3,20 +3,20 @@
/// JSON fixture file can be loaded directly using /// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler // the built-in JavaScript bundler
// @ts-ignore // @ts-ignore
const requiredExample = require('../../fixtures/example') const requiredExample = require("../../fixtures/example");
context('Files', () => { context("Files", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files') cy.visit("https://example.cypress.io/commands/files");
}) });
beforeEach(() => { beforeEach(() => {
// load example.json fixture file and store // load example.json fixture file and store
// in the test context object // in the test context object
cy.fixture('example.json').as('example') cy.fixture("example.json").as("example");
}) });
it('cy.fixture() - load a fixture', () => { it("cy.fixture() - load a fixture", () => {
// https://on.cypress.io/fixture // https://on.cypress.io/fixture
// Instead of writing a response inline you can // Instead of writing a response inline you can
@@ -24,65 +24,63 @@ context('Files', () => {
// when application makes an Ajax request matching "GET **/comments/*" // when application makes an Ajax request matching "GET **/comments/*"
// Cypress will intercept it and reply with the object in `example.json` fixture // Cypress will intercept it and reply with the object in `example.json` fixture
cy.intercept('GET', '**/comments/*', {fixture: 'example.json'}).as('getComment') cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.fixture-btn').click() cy.get(".fixture-btn").click();
cy.wait('@getComment').its('response.body') cy.wait("@getComment")
.should('have.property', 'name') .its("response.body")
.and('include', 'Using fixtures to represent data') .should("have.property", "name")
}) .and("include", "Using fixtures to represent data");
});
it('cy.fixture() or require - load a fixture', function () { it("cy.fixture() or require - load a fixture", function () {
// we are inside the "function () { ... }" // we are inside the "function () { ... }"
// callback and can use test context object "this" // callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback // "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context') expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
.to.deep.equal(requiredExample)
// or use "cy.wrap" and "should('deep.equal', ...)" assertion // or use "cy.wrap" and "should('deep.equal', ...)" assertion
cy.wrap(this.example) cy.wrap(this.example).should("deep.equal", requiredExample);
.should('deep.equal', requiredExample) });
})
it('cy.readFile() - read file contents', () => { it("cy.readFile() - read file contents", () => {
// https://on.cypress.io/readfile // https://on.cypress.io/readfile
// You can read a file and yield its contents // You can read a file and yield its contents
// The filePath is relative to your project's root. // The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => { cy.readFile("cypress.json").then((json) => {
expect(json).to.be.an('object') expect(json).to.be.an("object");
}) });
}) });
it('cy.writeFile() - write to a file', () => { it("cy.writeFile() - write to a file", () => {
// https://on.cypress.io/writefile // 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 // Use a response from a request to automatically
// generate a fixture file for use later // generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users') cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
.then((response) => { cy.writeFile("cypress/fixtures/users.json", response.body);
cy.writeFile('cypress/fixtures/users.json', response.body) });
})
cy.fixture('users').should((users) => { cy.fixture("users").should((users) => {
expect(users[0].name).to.exist expect(users[0].name).to.exist;
}) });
// JavaScript arrays and objects are stringified // JavaScript arrays and objects are stringified
// and formatted into text. // and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', { cy.writeFile("cypress/fixtures/profile.json", {
id: 8739, id: 8739,
name: 'Jane', name: "Jane",
email: 'jane@example.com', email: "jane@example.com"
}) });
cy.fixture('profile').should((profile) => { cy.fixture("profile").should((profile) => {
expect(profile.name).to.eq('Jane') expect(profile.name).to.eq("Jane");
}) });
}) });
}) });

View File

@@ -1,52 +1,58 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Local Storage', () => { context("Local Storage", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/local-storage') cy.visit("https://example.cypress.io/commands/local-storage");
}) });
// Although local storage is automatically cleared // Although local storage is automatically cleared
// in between tests to maintain a clean state // in between tests to maintain a clean state
// sometimes we need to clear the local storage manually // sometimes we need to clear the local storage manually
it('cy.clearLocalStorage() - clear all data in local storage', () => { it("cy.clearLocalStorage() - clear all data in local storage", () => {
// https://on.cypress.io/clearlocalstorage // https://on.cypress.io/clearlocalstorage
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') 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 // clearLocalStorage() yields the localStorage object
cy.clearLocalStorage().should((ls) => { cy.clearLocalStorage().should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.be.null expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem('prop3')).to.be.null expect(ls.getItem("prop3")).to.be.null;
}) });
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') 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 // Clear key matching string in Local Storage
cy.clearLocalStorage('prop1').should((ls) => { cy.clearLocalStorage("prop1").should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.eq('blue') expect(ls.getItem("prop2")).to.eq("blue");
expect(ls.getItem('prop3')).to.eq('magenta') expect(ls.getItem("prop3")).to.eq("magenta");
}) });
cy.get('.ls-btn').click().should(() => { cy.get(".ls-btn")
expect(localStorage.getItem('prop1')).to.eq('red') .click()
expect(localStorage.getItem('prop2')).to.eq('blue') .should(() => {
expect(localStorage.getItem('prop3')).to.eq('magenta') 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 // Clear keys matching regex in Local Storage
cy.clearLocalStorage(/prop1|2/).should((ls) => { cy.clearLocalStorage(/prop1|2/).should((ls) => {
expect(ls.getItem('prop1')).to.be.null expect(ls.getItem("prop1")).to.be.null;
expect(ls.getItem('prop2')).to.be.null expect(ls.getItem("prop2")).to.be.null;
expect(ls.getItem('prop3')).to.eq('magenta') expect(ls.getItem("prop3")).to.eq("magenta");
}) });
}) });
}) });

View File

@@ -1,32 +1,32 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Location', () => { context("Location", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/location') cy.visit("https://example.cypress.io/commands/location");
}) });
it('cy.hash() - get the current URL hash', () => { it("cy.hash() - get the current URL hash", () => {
// https://on.cypress.io/hash // https://on.cypress.io/hash
cy.hash().should('be.empty') cy.hash().should("be.empty");
}) });
it('cy.location() - get window.location', () => { it("cy.location() - get window.location", () => {
// https://on.cypress.io/location // https://on.cypress.io/location
cy.location().should((location) => { cy.location().should((location) => {
expect(location.hash).to.be.empty expect(location.hash).to.be.empty;
expect(location.href).to.eq('https://example.cypress.io/commands/location') expect(location.href).to.eq("https://example.cypress.io/commands/location");
expect(location.host).to.eq('example.cypress.io') expect(location.host).to.eq("example.cypress.io");
expect(location.hostname).to.eq('example.cypress.io') expect(location.hostname).to.eq("example.cypress.io");
expect(location.origin).to.eq('https://example.cypress.io') expect(location.origin).to.eq("https://example.cypress.io");
expect(location.pathname).to.eq('/commands/location') expect(location.pathname).to.eq("/commands/location");
expect(location.port).to.eq('') expect(location.port).to.eq("");
expect(location.protocol).to.eq('https:') expect(location.protocol).to.eq("https:");
expect(location.search).to.be.empty expect(location.search).to.be.empty;
}) });
}) });
it('cy.url() - get the current URL', () => { it("cy.url() - get the current URL", () => {
// https://on.cypress.io/url // https://on.cypress.io/url
cy.url().should('eq', 'https://example.cypress.io/commands/location') cy.url().should("eq", "https://example.cypress.io/commands/location");
}) });
}) });

View File

@@ -1,25 +1,25 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Misc', () => { context("Misc", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/misc') cy.visit("https://example.cypress.io/commands/misc");
}) });
it('.end() - end the command chain', () => { it(".end() - end the command chain", () => {
// https://on.cypress.io/end // https://on.cypress.io/end
// cy.end is useful when you want to end a chain of commands // cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element // and force Cypress to re-query from the root element
cy.get('.misc-table').within(() => { cy.get(".misc-table").within(() => {
// ends the current chain and yields null // ends the current chain and yields null
cy.contains('Cheryl').click().end() cy.contains("Cheryl").click().end();
// queries the entire table again // queries the entire table again
cy.contains('Charles').click() cy.contains("Charles").click();
}) });
}) });
it('cy.exec() - execute a system command', () => { it("cy.exec() - execute a system command", () => {
// execute a system command. // execute a system command.
// so you can take actions necessary for // so you can take actions necessary for
// your test outside the scope of Cypress. // your test outside the scope of Cypress.
@@ -28,79 +28,71 @@ context('Misc', () => {
// we can use Cypress.platform string to // we can use Cypress.platform string to
// select appropriate command // select appropriate command
// https://on.cypress/io/platform // https://on.cypress/io/platform
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
// on CircleCI Windows build machines we have a failure to run bash shell // on CircleCI Windows build machines we have a failure to run bash shell
// https://github.com/cypress-io/cypress/issues/5169 // https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true" // so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
if (isCircleOnWindows) { if (isCircleOnWindows) {
cy.log('Skipping test on CircleCI') cy.log("Skipping test on CircleCI");
return return;
} }
// cy.exec problem on Shippable CI // cy.exec problem on Shippable CI
// https://github.com/cypress-io/cypress/issues/6718 // https://github.com/cypress-io/cypress/issues/6718
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
if (isShippable) { if (isShippable) {
cy.log('Skipping test on ShippableCI') cy.log("Skipping test on ShippableCI");
return return;
} }
cy.exec('echo Jane Lane') cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
.its('stdout').should('contain', 'Jane Lane')
if (Cypress.platform === 'win32') { if (Cypress.platform === "win32") {
cy.exec('print cypress.json') cy.exec("print cypress.json").its("stderr").should("be.empty");
.its('stderr').should('be.empty')
} else { } else {
cy.exec('cat cypress.json') cy.exec("cat cypress.json").its("stderr").should("be.empty");
.its('stderr').should('be.empty')
cy.exec('pwd') cy.exec("pwd").its("code").should("eq", 0);
.its('code').should('eq', 0)
} }
}) });
it('cy.focused() - get the DOM element that has focus', () => { it("cy.focused() - get the DOM element that has focus", () => {
// https://on.cypress.io/focused // https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click() cy.get(".misc-form").find("#name").click();
cy.focused().should('have.id', 'name') cy.focused().should("have.id", "name");
cy.get('.misc-form').find('#description').click() cy.get(".misc-form").find("#description").click();
cy.focused().should('have.id', 'description') cy.focused().should("have.id", "description");
}) });
context('Cypress.Screenshot', function () { context("Cypress.Screenshot", function () {
it('cy.screenshot() - take a screenshot', () => { it("cy.screenshot() - take a screenshot", () => {
// https://on.cypress.io/screenshot // https://on.cypress.io/screenshot
cy.screenshot('my-image') cy.screenshot("my-image");
}) });
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { it("Cypress.Screenshot.defaults() - change default config of screenshots", function () {
Cypress.Screenshot.defaults({ Cypress.Screenshot.defaults({
blackout: ['.foo'], blackout: [".foo"],
capture: 'viewport', capture: "viewport",
clip: { x: 0, y: 0, width: 200, height: 200 }, clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false, scale: false,
disableTimersAndAnimations: true, disableTimersAndAnimations: true,
screenshotOnRunFailure: true, screenshotOnRunFailure: true,
onBeforeScreenshot() { onBeforeScreenshot() {},
}, onAfterScreenshot() {}
onAfterScreenshot() { });
}, });
}) });
})
})
it('cy.wrap() - wrap an object', () => { it("cy.wrap() - wrap an object", () => {
// https://on.cypress.io/wrap // https://on.cypress.io/wrap
cy.wrap({foo: 'bar'}) cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
.should('have.property', 'foo') });
.and('include', 'bar') });
})
})

View File

@@ -1,56 +1,56 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Navigation', () => { context("Navigation", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io') cy.visit("https://example.cypress.io");
cy.get('.navbar-nav').contains('Commands').click() cy.get(".navbar-nav").contains("Commands").click();
cy.get('.dropdown-menu').contains('Navigation').click() cy.get(".dropdown-menu").contains("Navigation").click();
}) });
it('cy.go() - go back or forward in the browser\'s history', () => { it("cy.go() - go back or forward in the browser's history", () => {
// https://on.cypress.io/go // https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
cy.go('back') cy.go("back");
cy.location('pathname').should('not.include', 'navigation') cy.location("pathname").should("not.include", "navigation");
cy.go('forward') cy.go("forward");
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
// clicking back // clicking back
cy.go(-1) cy.go(-1);
cy.location('pathname').should('not.include', 'navigation') cy.location("pathname").should("not.include", "navigation");
// clicking forward // clicking forward
cy.go(1) cy.go(1);
cy.location('pathname').should('include', 'navigation') cy.location("pathname").should("include", "navigation");
}) });
it('cy.reload() - reload the page', () => { it("cy.reload() - reload the page", () => {
// https://on.cypress.io/reload // https://on.cypress.io/reload
cy.reload() cy.reload();
// reload the page without using the cache // reload the page without using the cache
cy.reload(true) cy.reload(true);
}) });
it('cy.visit() - visit a remote url', () => { it("cy.visit() - visit a remote url", () => {
// https://on.cypress.io/visit // 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 // Pass options to the visit
cy.visit('https://example.cypress.io/commands/navigation', { cy.visit("https://example.cypress.io/commands/navigation", {
timeout: 50000, // increase total time for the visit to resolve timeout: 50000, // increase total time for the visit to resolve
onBeforeLoad(contentWindow) { onBeforeLoad(contentWindow) {
// contentWindow is the remote page's window object // contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true expect(typeof contentWindow === "object").to.be.true;
}, },
onLoad(contentWindow) { onLoad(contentWindow) {
// contentWindow is the remote page's window object // contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true expect(typeof contentWindow === "object").to.be.true;
}, }
}) });
}) });
}) });

View File

@@ -1,96 +1,94 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Network Requests', () => { context("Network Requests", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/network-requests') cy.visit("https://example.cypress.io/commands/network-requests");
}) });
// Manage HTTP requests in your app // Manage HTTP requests in your app
it('cy.request() - make an XHR request', () => { it("cy.request() - make an XHR request", () => {
// https://on.cypress.io/request // https://on.cypress.io/request
cy.request('https://jsonplaceholder.cypress.io/comments') cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
.should((response) => { expect(response.status).to.eq(200);
expect(response.status).to.eq(200)
// the server sometimes gets an extra comment posted from another machine // the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object // which gets returned as 1 extra object
expect(response.body).to.have.property('length').and.be.oneOf([500, 501]) expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.have.property('headers') expect(response).to.have.property("headers");
expect(response).to.have.property('duration') expect(response).to.have.property("duration");
}) });
}) });
it('cy.request() - verify response using BDD syntax', () => { it("cy.request() - verify response using BDD syntax", () => {
cy.request('https://jsonplaceholder.cypress.io/comments') cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
.then((response) => {
// https://on.cypress.io/assertions // https://on.cypress.io/assertions
expect(response).property('status').to.equal(200) expect(response).property("status").to.equal(200);
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501]) expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.include.keys('headers', 'duration') expect(response).to.include.keys("headers", "duration");
}) });
}) });
it('cy.request() with query parameters', () => { it("cy.request() with query parameters", () => {
// will execute request // will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3 // https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({ cy.request({
url: 'https://jsonplaceholder.cypress.io/comments', url: "https://jsonplaceholder.cypress.io/comments",
qs: { qs: {
postId: 1, postId: 1,
id: 3, id: 3
}, }
}) })
.its('body') .its("body")
.should('be.an', 'array') .should("be.an", "array")
.and('have.length', 1) .and("have.length", 1)
.its('0') // yields first element of the array .its("0") // yields first element of the array
.should('contain', { .should("contain", {
postId: 1, postId: 1,
id: 3, id: 3
}) });
}) });
it('cy.request() - pass result to the second request', () => { it("cy.request() - pass result to the second request", () => {
// first, let's find out the userId of the first user we have // first, let's find out the userId of the first user we have
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its('body') // yields the response object .its("body") // yields the response object
.its('0') // yields the first element of the returned list .its("0") // yields the first element of the returned list
// the above two commands its('body').its('0') // the above two commands its('body').its('0')
// can be written as its('body.0') // can be written as its('body.0')
// if you do not care about TypeScript checks // if you do not care about TypeScript checks
.then((user) => { .then((user) => {
expect(user).property('id').to.be.a('number') expect(user).property("id").to.be.a("number");
// make a new post on behalf of the user // make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: user.id, userId: user.id,
title: 'Cypress Test Runner', title: "Cypress Test Runner",
body: 'Fast, easy and reliable testing for anything that runs in a browser.', 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 // note that the value here is the returned value of the 2nd request
// which is the new post object // which is the new post object
.then((response) => { .then((response) => {
expect(response).property('status').to.equal(201) // new entity created expect(response).property("status").to.equal(201); // new entity created
expect(response).property('body').to.contain({ expect(response).property("body").to.contain({
title: 'Cypress Test Runner', title: "Cypress Test Runner"
}) });
// we don't know the exact post id - only that it will be > 100 // we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts // since JSONPlaceholder has built-in 100 posts
expect(response.body).property('id').to.be.a('number') expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
.and.to.be.gt(100)
// we don't know the user id here - since it was in above closure // 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 // so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number') expect(response.body).property("userId").to.be.a("number");
}) });
}) });
it('cy.request() - save response in the shared test context', () => { it("cy.request() - save response in the shared test context", () => {
// https://on.cypress.io/variables-and-aliases // https://on.cypress.io/variables-and-aliases
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its('body').its('0') // yields the first element of the returned list .its("body")
.as('user') // saves the object in the test context .its("0") // yields the first element of the returned list
.as("user") // saves the object in the test context
.then(function () { .then(function () {
// NOTE 👀 // NOTE 👀
// By the time this callback runs the "as('user')" command // By the time this callback runs the "as('user')" command
@@ -98,66 +96,70 @@ context('Network Requests', () => {
// To access the test context we need to use // To access the test context we need to use
// the "function () { ... }" callback form, // the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object! // otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: this.user.id, userId: this.user.id,
title: 'Cypress Test Runner', title: "Cypress Test Runner",
body: 'Fast, easy and reliable testing for anything that runs in a browser.', body: "Fast, easy and reliable testing for anything that runs in a browser."
}) })
.its('body').as('post') // save the new post from the response .its("body")
.as("post"); // save the new post from the response
}) })
.then(function () { .then(function () {
// When this callback runs, both "cy.request" API commands have finished // When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set. // and the test context has "user" and "post" objects set.
// Let's verify them. // Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id) expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id);
}) });
}) });
it('cy.intercept() - route responses to matching requests', () => { it("cy.intercept() - route responses to matching requests", () => {
// https://on.cypress.io/intercept // https://on.cypress.io/intercept
let message = 'whoa, this comment does not exist' let message = "whoa, this comment does not exist";
// Listen to GET to comments/1 // Listen to GET to comments/1
cy.intercept('GET', '**/comments/*').as('getComment') cy.intercept("GET", "**/comments/*").as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-btn').click() cy.get(".network-btn").click();
// https://on.cypress.io/wait // https://on.cypress.io/wait
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
// Listen to POST to comments // Listen to POST to comments
cy.intercept('POST', '**/comments').as('postComment') cy.intercept("POST", "**/comments").as("postComment");
// we have code that posts a comment when // we have code that posts a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-post').click() cy.get(".network-post").click();
cy.wait('@postComment').should(({request, response}) => { cy.wait("@postComment").should(({ request, response }) => {
expect(request.body).to.include('email') expect(request.body).to.include("email");
expect(request.headers).to.have.property('content-type') expect(request.headers).to.have.property("content-type");
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()') expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
}) });
// Stub a response to PUT comments/ **** // Stub a response to PUT comments/ ****
cy.intercept({ cy.intercept(
method: 'PUT', {
url: '**/comments/*', method: "PUT",
}, { url: "**/comments/*"
},
{
statusCode: 404, statusCode: 404,
body: { error: message }, body: { error: message },
headers: {'access-control-allow-origin': '*'}, headers: { "access-control-allow-origin": "*" },
delayMs: 500, delayMs: 500
}).as('putComment') }
).as("putComment");
// we have code that puts a comment when // we have code that puts a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-put').click() cy.get(".network-put").click();
cy.wait('@putComment') cy.wait("@putComment");
// our 404 statusCode logic in scripts.js executed // our 404 statusCode logic in scripts.js executed
cy.get('.network-put-comment').should('contain', message) cy.get(".network-put-comment").should("contain", message);
}) });
}) });

View File

@@ -1,114 +1,100 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Querying', () => { context("Querying", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/querying') cy.visit("https://example.cypress.io/commands/querying");
}) });
// The most commonly used query is 'cy.get()', you can // The most commonly used query is 'cy.get()', you can
// think of this like the '$' in jQuery // think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => { it("cy.get() - query DOM elements", () => {
// https://on.cypress.io/get // 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') cy.get("#querying .well>button:first").should("contain", "Button");
// ↲ // ↲
// Use CSS selectors just like jQuery // 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 // 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method // by invoking `.attr()` method
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
// or you can get element's CSS property // or you can get element's CSS property
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
.invoke('css', 'position')
.should('equal', 'static')
// or use assertions directly during 'cy.get()' // or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions // https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]') cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example') .should("have.attr", "data-test-id", "test-example")
.and('have.css', 'position', 'static') .and("have.css", "position", "static");
}) });
it('cy.contains() - query DOM elements with matching content', () => { it("cy.contains() - query DOM elements with matching content", () => {
// https://on.cypress.io/contains // https://on.cypress.io/contains
cy.get('.query-list') cy.get(".query-list").contains("bananas").should("have.class", "third");
.contains('bananas')
.should('have.class', 'third')
// we can pass a regexp to `.contains()` // we can pass a regexp to `.contains()`
cy.get('.query-list') cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get('.query-list') cy.get(".query-list").contains("apples").should("have.class", "first");
.contains('apples')
.should('have.class', 'first')
// passing a selector to contains will // passing a selector to contains will
// yield the selector containing the text // yield the selector containing the text
cy.get('#querying') cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get('.query-button') cy.get(".query-button").contains("Save Form").should("have.class", "btn");
.contains('Save Form') });
.should('have.class', 'btn')
})
it('.within() - query DOM elements within a specific element', () => { it(".within() - query DOM elements within a specific element", () => {
// https://on.cypress.io/within // https://on.cypress.io/within
cy.get('.query-form').within(() => { cy.get(".query-form").within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email') cy.get("input:first").should("have.attr", "placeholder", "Email");
cy.get('input:last').should('have.attr', 'placeholder', 'Password') cy.get("input:last").should("have.attr", "placeholder", "Password");
}) });
}) });
it('cy.root() - query the root DOM element', () => { it("cy.root() - query the root DOM element", () => {
// https://on.cypress.io/root // https://on.cypress.io/root
// By default, root is the document // By default, root is the document
cy.root().should('match', 'html') cy.root().should("match", "html");
cy.get('.query-ul').within(() => { cy.get(".query-ul").within(() => {
// In this within, the root is now the ul DOM element // In this within, the root is now the ul DOM element
cy.root().should('have.class', 'query-ul') cy.root().should("have.class", "query-ul");
}) });
}) });
it('best practices - selecting elements', () => { it("best practices - selecting elements", () => {
// https://on.cypress.io/best-practices#Selecting-Elements // https://on.cypress.io/best-practices#Selecting-Elements
cy.get('[data-cy=best-practices-selecting-elements]').within(() => { cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
// Worst - too generic, no context // Worst - too generic, no context
cy.get('button').click() cy.get("button").click();
// Bad. Coupled to styling. Highly subject to change. // Bad. Coupled to styling. Highly subject to change.
cy.get('.btn.btn-large').click() cy.get(".btn.btn-large").click();
// Average. Coupled to the `name` attribute which has HTML semantics. // Average. Coupled to the `name` attribute which has HTML semantics.
cy.get('[name=submission]').click() cy.get("[name=submission]").click();
// Better. But still coupled to styling or JS event listeners. // Better. But still coupled to styling or JS event listeners.
cy.get('#main').click() cy.get("#main").click();
// Slightly better. Uses an ID but also ensures the element // Slightly better. Uses an ID but also ensures the element
// has an ARIA role attribute // has an ARIA role attribute
cy.get('#main[role=button]').click() cy.get("#main[role=button]").click();
// Much better. But still coupled to text content that may change. // Much better. But still coupled to text content that may change.
cy.contains('Submit').click() cy.contains("Submit").click();
// Best. Insulated from all changes. // Best. Insulated from all changes.
cy.get('[data-cy=submit]').click() cy.get("[data-cy=submit]").click();
}) });
}) });
}) });

View File

@@ -2,25 +2,24 @@
// remove no check once Cypress.sinon is typed // remove no check once Cypress.sinon is typed
// https://github.com/cypress-io/cypress/issues/6720 // https://github.com/cypress-io/cypress/issues/6720
context('Spies, Stubs, and Clock', () => { context("Spies, Stubs, and Clock", () => {
it('cy.spy() - wrap a method in a spy', () => { it("cy.spy() - wrap a method in a spy", () => {
// https://on.cypress.io/spy // https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
foo() { 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', () => { it("cy.spy() retries until assertions pass", () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
/** /**
@@ -28,26 +27,26 @@ context('Spies, Stubs, and Clock', () => {
* @param x {any} * @param x {any}
*/ */
foo(x) { foo(x) {
console.log('obj.foo called with', x) console.log("obj.foo called with", x);
},
} }
};
cy.spy(obj, 'foo').as('foo') cy.spy(obj, "foo").as("foo");
setTimeout(() => { setTimeout(() => {
obj.foo('first') obj.foo("first");
}, 500) }, 500);
setTimeout(() => { setTimeout(() => {
obj.foo('second') obj.foo("second");
}, 2500) }, 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', () => { it("cy.stub() - create a stub and/or replace a function with stub", () => {
// https://on.cypress.io/stub // https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = { const obj = {
/** /**
@@ -56,48 +55,45 @@ context('Spies, Stubs, and Clock', () => {
* @param b {string} * @param b {string}
*/ */
foo(a, b) { foo(a, b) {
console.log('a', a, 'b', 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', () => { it("cy.clock() - control time in the browser", () => {
// https://on.cypress.io/clock // https://on.cypress.io/clock
// create the date in UTC so its always the same // create the date in UTC so its always the same
// no matter what local timezone the browser is running in // no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime() const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now) cy.clock(now);
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get('#clock-div').click() cy.get("#clock-div").click().should("have.text", "1489449600");
.should('have.text', '1489449600') });
})
it('cy.tick() - move time in the browser', () => { it("cy.tick() - move time in the browser", () => {
// https://on.cypress.io/tick // https://on.cypress.io/tick
// create the date in UTC so its always the same // create the date in UTC so its always the same
// no matter what local timezone the browser is running in // no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime() const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now) cy.clock(now);
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get('#tick-div').click() cy.get("#tick-div").click().should("have.text", "1489449600");
.should('have.text', '1489449600')
cy.tick(10000) // 10 seconds passed cy.tick(10000); // 10 seconds passed
cy.get('#tick-div').click() cy.get("#tick-div").click().should("have.text", "1489449610");
.should('have.text', '1489449610') });
})
it('cy.stub() matches depending on arguments', () => { it("cy.stub() matches depending on arguments", () => {
// see all possible matchers at // see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/ // https://sinonjs.org/releases/latest/matchers/
const greeter = { const greeter = {
@@ -106,26 +102,28 @@ context('Spies, Stubs, and Clock', () => {
* @param {string} name * @param {string} name
*/ */
greet(name) { greet(name) {
return `Hello, ${name}!` return `Hello, ${name}!`;
},
} }
};
cy.stub(greeter, 'greet') cy.stub(greeter, "greet")
.callThrough() // if you want non-matched calls to call the real method .callThrough() // if you want non-matched calls to call the real method
.withArgs(Cypress.sinon.match.string).returns('Hi') .withArgs(Cypress.sinon.match.string)
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name')) .returns("Hi")
.withArgs(Cypress.sinon.match.number)
.throws(new Error("Invalid name"));
expect(greeter.greet('World')).to.equal('Hi') expect(greeter.greet("World")).to.equal("Hi");
// @ts-ignore // @ts-ignore
expect(() => greeter.greet(42)).to.throw('Invalid name') expect(() => greeter.greet(42)).to.throw("Invalid name");
expect(greeter.greet).to.have.been.calledTwice expect(greeter.greet).to.have.been.calledTwice;
// non-matched calls goes the actual method // non-matched calls goes the actual method
// @ts-ignore // @ts-ignore
expect(greeter.greet()).to.equal('Hello, undefined!') expect(greeter.greet()).to.equal("Hello, undefined!");
}) });
it('matches call arguments using Sinon matchers', () => { it("matches call arguments using Sinon matchers", () => {
// see all possible matchers at // see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/ // https://sinonjs.org/releases/latest/matchers/
const calculator = { const calculator = {
@@ -135,72 +133,71 @@ context('Spies, Stubs, and Clock', () => {
* @param b {number} * @param b {number}
*/ */
add(a, b) { add(a, b) {
return 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 // if we want to assert the exact values used during the call
expect(spy).to.be.calledWith(2, 3) expect(spy).to.be.calledWith(2, 3);
// let's confirm "add" method was called with two numbers // let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number) expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
// alternatively, provide the value to match // alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)) expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
// match any value // match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3) expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
// match any value from a list // match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3) expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
/** /**
* Returns true if the given number is event * Returns true if the given number is event
* @param {number} x * @param {number} x
*/ */
const isEven = (x) => x % 2 === 0 const isEven = (x) => x % 2 === 0;
// expect the value to pass a custom predicate function // expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is // the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails // shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3) 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 * Returns a function that checks if a given number is larger than the limit
* @param {number} limit * @param {number} limit
* @returns {(x: number) => boolean} * @returns {(x: number) => boolean}
*/ */
const isGreaterThan = (limit) => (x) => x > limit const isGreaterThan = (limit) => (x) => x > limit;
/** /**
* Returns a function that checks if a given number is less than the limit * Returns a function that checks if a given number is less than the limit
* @param {number} limit * @param {number} limit
* @returns {(x: number) => boolean} * @returns {(x: number) => boolean}
*/ */
const isLessThan = (limit) => (x) => x < limit const isLessThan = (limit) => (x) => x < limit;
// you can combine several matchers using "and", "or" // you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith( expect(spy).to.be.calledWith(
Cypress.sinon.match.number, Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')), Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
) );
expect(spy).to.be.calledWith( expect(spy).to.be.calledWith(
Cypress.sinon.match.number, Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)), Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
) );
// matchers can be used from BDD assertions // matchers can be used from BDD assertions
cy.get('@add').should('have.been.calledWith', cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
Cypress.sinon.match.number, Cypress.sinon.match(3))
// you can alias matchers for shorter test code // you can alias matchers for shorter test code
const {match: M} = Cypress.sinon 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));
}) });
}) });

View File

@@ -1,121 +1,97 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Traversal', () => { context("Traversal", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/traversal') cy.visit("https://example.cypress.io/commands/traversal");
}) });
it('.children() - get child DOM elements', () => { it(".children() - get child DOM elements", () => {
// https://on.cypress.io/children // https://on.cypress.io/children
cy.get('.traversal-breadcrumb') cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
.children('.active') });
.should('contain', 'Data')
})
it('.closest() - get closest ancestor DOM element', () => { it(".closest() - get closest ancestor DOM element", () => {
// https://on.cypress.io/closest // https://on.cypress.io/closest
cy.get('.traversal-badge') cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
.closest('ul') });
.should('have.class', 'list-group')
})
it('.eq() - get a DOM element at a specific index', () => { it(".eq() - get a DOM element at a specific index", () => {
// https://on.cypress.io/eq // https://on.cypress.io/eq
cy.get('.traversal-list>li') cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
.eq(1).should('contain', 'siamese') });
})
it('.filter() - get DOM elements that match the selector', () => { it(".filter() - get DOM elements that match the selector", () => {
// https://on.cypress.io/filter // https://on.cypress.io/filter
cy.get('.traversal-nav>li') cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
.filter('.active').should('contain', 'About') });
})
it('.find() - get descendant DOM elements of the selector', () => { it(".find() - get descendant DOM elements of the selector", () => {
// https://on.cypress.io/find // https://on.cypress.io/find
cy.get('.traversal-pagination') cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
.find('li').find('a') });
.should('have.length', 7)
})
it('.first() - get first DOM element', () => { it(".first() - get first DOM element", () => {
// https://on.cypress.io/first // https://on.cypress.io/first
cy.get('.traversal-table td') cy.get(".traversal-table td").first().should("contain", "1");
.first().should('contain', '1') });
})
it('.last() - get last DOM element', () => { it(".last() - get last DOM element", () => {
// https://on.cypress.io/last // https://on.cypress.io/last
cy.get('.traversal-buttons .btn') cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
.last().should('contain', 'Submit') });
})
it('.next() - get next sibling DOM element', () => { it(".next() - get next sibling DOM element", () => {
// https://on.cypress.io/next // https://on.cypress.io/next
cy.get('.traversal-ul') cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
.contains('apples').next().should('contain', 'oranges') });
})
it('.nextAll() - get all next sibling DOM elements', () => { it(".nextAll() - get all next sibling DOM elements", () => {
// https://on.cypress.io/nextall // https://on.cypress.io/nextall
cy.get('.traversal-next-all') cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
.contains('oranges') });
.nextAll().should('have.length', 3)
})
it('.nextUntil() - get next sibling DOM elements until next el', () => { it(".nextUntil() - get next sibling DOM elements until next el", () => {
// https://on.cypress.io/nextuntil // https://on.cypress.io/nextuntil
cy.get('#veggies') cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
.nextUntil('#nuts').should('have.length', 3) });
})
it('.not() - remove DOM elements from set of DOM elements', () => { it(".not() - remove DOM elements from set of DOM elements", () => {
// https://on.cypress.io/not // https://on.cypress.io/not
cy.get('.traversal-disabled .btn') cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
.not('[disabled]').should('not.contain', 'Disabled') });
})
it('.parent() - get parent DOM element from DOM elements', () => { it(".parent() - get parent DOM element from DOM elements", () => {
// https://on.cypress.io/parent // https://on.cypress.io/parent
cy.get('.traversal-mark') cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
.parent().should('contain', 'Morbi leo risus') });
})
it('.parents() - get parent DOM elements from DOM elements', () => { it(".parents() - get parent DOM elements from DOM elements", () => {
// https://on.cypress.io/parents // https://on.cypress.io/parents
cy.get('.traversal-cite') cy.get(".traversal-cite").parents().should("match", "blockquote");
.parents().should('match', 'blockquote') });
})
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
// https://on.cypress.io/parentsuntil // https://on.cypress.io/parentsuntil
cy.get('.clothes-nav') cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
.find('.active') });
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
it('.prev() - get previous sibling DOM element', () => { it(".prev() - get previous sibling DOM element", () => {
// https://on.cypress.io/prev // https://on.cypress.io/prev
cy.get('.birds').find('.active') cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
.prev().should('contain', 'Lorikeets') });
})
it('.prevAll() - get all previous sibling DOM elements', () => { it(".prevAll() - get all previous sibling DOM elements", () => {
// https://on.cypress.io/prevall // https://on.cypress.io/prevall
cy.get('.fruits-list').find('.third') cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
.prevAll().should('have.length', 2) });
})
it('.prevUntil() - get all previous sibling DOM elements until el', () => { it(".prevUntil() - get all previous sibling DOM elements until el", () => {
// https://on.cypress.io/prevuntil // https://on.cypress.io/prevuntil
cy.get('.foods-list').find('#nuts') cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
.prevUntil('#veggies').should('have.length', 3) });
})
it('.siblings() - get all sibling DOM elements', () => { it(".siblings() - get all sibling DOM elements", () => {
// https://on.cypress.io/siblings // https://on.cypress.io/siblings
cy.get('.traversal-pills .active') cy.get(".traversal-pills .active").siblings().should("have.length", 2);
.siblings().should('have.length', 2) });
}) });
})

View File

@@ -1,84 +1,82 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Utilities', () => { context("Utilities", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/utilities') cy.visit("https://example.cypress.io/utilities");
}) });
it('Cypress._ - call a lodash method', () => { it("Cypress._ - call a lodash method", () => {
// https://on.cypress.io/_ // https://on.cypress.io/_
cy.request('https://jsonplaceholder.cypress.io/users') cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
.then((response) => { let ids = Cypress._.chain(response.body).map("id").take(3).value();
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', () => { it("Cypress.$ - call a jQuery method", () => {
// https://on.cypress.io/$ // https://on.cypress.io/$
let $li = Cypress.$('.utility-jquery li:first') let $li = Cypress.$(".utility-jquery li:first");
cy.wrap($li) cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active");
.should('not.have.class', 'active') });
.click()
.should('have.class', 'active')
})
it('Cypress.Blob - blob utilities and base64 string conversion', () => { it("Cypress.Blob - blob utilities and base64 string conversion", () => {
// https://on.cypress.io/blob // https://on.cypress.io/blob
cy.get('.utility-blob').then(($div) => { cy.get(".utility-blob").then(($div) => {
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL // https://github.com/nolanlawson/blob-util#imgSrcToDataURL
// get the dataUrl string for the javascript-logo // get the dataUrl string for the javascript-logo
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') return Cypress.Blob.imgSrcToDataURL(
.then((dataUrl) => { "https://example.cypress.io/assets/img/javascript-logo.png",
undefined,
"anonymous"
).then((dataUrl) => {
// create an <img> element and set its src to the dataUrl // create an <img> element and set its src to the dataUrl
let img = Cypress.$('<img />', {src: dataUrl}) let img = Cypress.$("<img />", { src: dataUrl });
// need to explicitly return cy here since we are initially returning // need to explicitly return cy here since we are initially returning
// the Cypress.Blob.imgSrcToDataURL promise to our test // the Cypress.Blob.imgSrcToDataURL promise to our test
// append the image // append the image
$div.append(img) $div.append(img);
cy.get('.utility-blob img').click() cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl);
.should('have.attr', 'src', dataUrl) });
}) });
}) });
})
it('Cypress.minimatch - test out glob patterns against strings', () => { it("Cypress.minimatch - test out glob patterns against strings", () => {
// https://on.cypress.io/minimatch // https://on.cypress.io/minimatch
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
matchBase: true, 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', { matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
matchBase: true, matchBase: true
}) });
expect(matching, 'comments').to.be.false expect(matching, "comments").to.be.false;
// ** matches against all downstream path segments // ** matches against all downstream path segments
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
matchBase: true, 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/*', { matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
matchBase: false, matchBase: false
}) });
expect(matching, 'comments').to.be.false expect(matching, "comments").to.be.false;
}) });
it('Cypress.Promise - instantiate a bluebird promise', () => { it("Cypress.Promise - instantiate a bluebird promise", () => {
// https://on.cypress.io/promise // https://on.cypress.io/promise
let waited = false let waited = false;
/** /**
* @return Bluebird<string> * @return Bluebird<string>
@@ -89,12 +87,12 @@ context('Utilities', () => {
return new Cypress.Promise((resolve, reject) => { return new Cypress.Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
// set waited to true // set waited to true
waited = true waited = true;
// resolve with 'foo' string // resolve with 'foo' string
resolve('foo') resolve("foo");
}, 1000) }, 1000);
}) });
} }
cy.then(() => { cy.then(() => {
@@ -102,9 +100,9 @@ context('Utilities', () => {
// is awaited until it resolves // is awaited until it resolves
// @ts-ignore TS7006 // @ts-ignore TS7006
return waitOneSecond().then((str) => { return waitOneSecond().then((str) => {
expect(str).to.eq('foo') expect(str).to.eq("foo");
expect(waited).to.be.true expect(waited).to.be.true;
}) });
}) });
}) });
}) });

View File

@@ -1,23 +1,23 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Viewport', () => { context("Viewport", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/viewport') cy.visit("https://example.cypress.io/commands/viewport");
}) });
it('cy.viewport() - set the viewport size and dimension', () => { it("cy.viewport() - set the viewport size and dimension", () => {
// https://on.cypress.io/viewport // https://on.cypress.io/viewport
cy.get('#navbar').should('be.visible') cy.get("#navbar").should("be.visible");
cy.viewport(320, 480) cy.viewport(320, 480);
// the navbar should have collapse since our screen is smaller // the navbar should have collapse since our screen is smaller
cy.get('#navbar').should('not.be.visible') cy.get("#navbar").should("not.be.visible");
cy.get('.navbar-toggle').should('be.visible').click() cy.get(".navbar-toggle").should("be.visible").click();
cy.get('.nav').find('a').should('be.visible') cy.get(".nav").find("a").should("be.visible");
// lets see what our app looks like on a super large screen // lets see what our app looks like on a super large screen
cy.viewport(2999, 2999) cy.viewport(2999, 2999);
// cy.viewport() accepts a set of preset sizes // cy.viewport() accepts a set of preset sizes
// to easily set the screen to a device's width and height // to easily set the screen to a device's width and height
@@ -25,35 +25,35 @@ context('Viewport', () => {
// We added a cy.wait() between each viewport change so you can 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 :) // the change otherwise it is a little too fast to see :)
cy.viewport('macbook-15') cy.viewport("macbook-15");
cy.wait(200) cy.wait(200);
cy.viewport('macbook-13') cy.viewport("macbook-13");
cy.wait(200) cy.wait(200);
cy.viewport('macbook-11') cy.viewport("macbook-11");
cy.wait(200) cy.wait(200);
cy.viewport('ipad-2') cy.viewport("ipad-2");
cy.wait(200) cy.wait(200);
cy.viewport('ipad-mini') cy.viewport("ipad-mini");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-6+') cy.viewport("iphone-6+");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-6') cy.viewport("iphone-6");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-5') cy.viewport("iphone-5");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-4') cy.viewport("iphone-4");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-3') cy.viewport("iphone-3");
cy.wait(200) cy.wait(200);
// cy.viewport() accepts an orientation for all presets // cy.viewport() accepts an orientation for all presets
// the default orientation is 'portrait' // the default orientation is 'portrait'
cy.viewport('ipad-2', 'portrait') cy.viewport("ipad-2", "portrait");
cy.wait(200) cy.wait(200);
cy.viewport('iphone-4', 'landscape') cy.viewport("iphone-4", "landscape");
cy.wait(200) cy.wait(200);
// The viewport will be reset back to the default dimensions // The viewport will be reset back to the default dimensions
// in between tests (the default can be set in cypress.json) // in between tests (the default can be set in cypress.json)
}) });
}) });

View File

@@ -1,31 +1,31 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Waiting', () => { context("Waiting", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting') cy.visit("https://example.cypress.io/commands/waiting");
}) });
// BE CAREFUL of adding unnecessary wait times. // BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting // https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait // https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => { it("cy.wait() - wait for a specific amount of time", () => {
cy.get('.wait-input1').type('Wait 1000ms after typing') cy.get(".wait-input1").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
cy.get('.wait-input2').type('Wait 1000ms after typing') cy.get(".wait-input2").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
cy.get('.wait-input3').type('Wait 1000ms after typing') cy.get(".wait-input3").type("Wait 1000ms after typing");
cy.wait(1000) cy.wait(1000);
}) });
it('cy.wait() - wait for a specific route', () => { it("cy.wait() - wait for a specific route", () => {
// Listen to GET to comments/1 // Listen to GET to comments/1
cy.intercept('GET', '**/comments/*').as('getComment') cy.intercept("GET", "**/comments/*").as("getComment");
// we have code that gets a comment when // we have code that gets a comment when
// the button is clicked in scripts.js // the button is clicked in scripts.js
cy.get('.network-btn').click() cy.get(".network-btn").click();
// wait for GET comments/1 // wait for GET comments/1
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304]) cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
}) });
}) });

View File

@@ -1,22 +1,22 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
context('Window', () => { context("Window", () => {
beforeEach(() => { beforeEach(() => {
cy.visit('https://example.cypress.io/commands/window') cy.visit("https://example.cypress.io/commands/window");
}) });
it('cy.window() - get the global window object', () => { it("cy.window() - get the global window object", () => {
// https://on.cypress.io/window // https://on.cypress.io/window
cy.window().should('have.property', 'top') cy.window().should("have.property", "top");
}) });
it('cy.document() - get the document object', () => { it("cy.document() - get the document object", () => {
// https://on.cypress.io/document // https://on.cypress.io/document
cy.document().should('have.property', 'charset').and('eq', 'UTF-8') cy.document().should("have.property", "charset").and("eq", "UTF-8");
}) });
it('cy.title() - get the title', () => { it("cy.title() - get the title", () => {
// https://on.cypress.io/title // https://on.cypress.io/title
cy.title().should('include', 'Kitchen Sink') cy.title().should("include", "Kitchen Sink");
}) });
}) });

View File

@@ -19,4 +19,4 @@
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config
} };

View File

@@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "./commands";
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

View File

@@ -2,11 +2,7 @@
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"baseUrl": "../node_modules", "baseUrl": "../node_modules",
"types": [ "types": ["cypress"]
"cypress"
]
}, },
"include": [ "include": ["**/*.*"]
"**/*.*"
]
} }

View File

@@ -1 +1,2 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' }) if ("serviceWorker" in navigator)
navigator.serviceWorker.register("/dev-sw.js?dev-sw", { scope: "/", type: "classic" });

View File

@@ -21,9 +21,9 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => { const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href; uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || ( return (
registry[uri] ||
new Promise(resolve => { new Promise((resolve) => {
if ("document" in self) { if ("document" in self) {
const script = document.createElement("script"); const script = document.createElement("script");
script.src = uri; script.src = uri;
@@ -34,9 +34,7 @@ if (!self.define) {
importScripts(uri); importScripts(uri);
resolve(); resolve();
} }
}) }).then(() => {
.then(() => {
let promise = registry[uri]; let promise = registry[uri];
if (!promise) { if (!promise) {
throw new Error(`Module ${uri} didnt register its module`); throw new Error(`Module ${uri} didnt register its module`);
@@ -53,21 +51,20 @@ if (!self.define) {
return; return;
} }
let exports = {}; let exports = {};
const require = depUri => singleRequire(depUri, uri); const require = (depUri) => singleRequire(depUri, uri);
const specialDeps = { const specialDeps = {
module: { uri }, module: { uri },
exports, exports,
require require
}; };
registry[uri] = Promise.all(depsNames.map( registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps); factory(...deps);
return exports; return exports;
}); });
}; };
} }
define(['./workbox-b5f7729d'], (function (workbox) { 'use strict'; define(["./workbox-b5f7729d"], function (workbox) {
"use strict";
self.skipWaiting(); self.skipWaiting();
workbox.clientsClaim(); workbox.clientsClaim();
@@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
* requests for URLs in the manifest. * requests for URLs in the manifest.
* See https://goo.gl/S9QRab * See https://goo.gl/S9QRab
*/ */
workbox.precacheAndRoute([{ workbox.precacheAndRoute(
"url": "registerSW.js", [
"revision": "3ca0b8505b4bec776b69afdba2768812" {
}, { url: "registerSW.js",
"url": "index.html", revision: "3ca0b8505b4bec776b69afdba2768812"
"revision": "0.sa702m4aq68" },
}], {}); {
url: "index.html",
revision: "0.sa702m4aq68"
}
],
{}
);
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(
new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/] allowlist: [/^\/$/]
})); })
);
})); });

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react'; import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day"; import dayjs from "../utils/day";
import 'dayjs/locale/en'; import "dayjs/locale/en";
import React from "react"; 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 GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
@@ -12,11 +12,11 @@ import App from "./App";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider"; import themeProvider from "./themeProvider";
import { Userpilot } from 'userpilot' import { Userpilot } from "userpilot";
// Initialize Userpilot // Initialize Userpilot
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
Userpilot.initialize('NX-69145f08'); Userpilot.initialize("NX-69145f08");
} }
dayjs.locale("en"); dayjs.locale("en");
@@ -24,12 +24,11 @@ dayjs.locale("en");
const config = { const config = {
core: { core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API, authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon", key: "anon"
}, }
}; };
export const factory = SplitSdk(config); export const factory = SplitSdk(config);
function AppContainer() { function AppContainer() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -43,8 +42,8 @@ function AppContainer() {
form={{ form={{
validateMessages: { validateMessages: {
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}), required: t("general.validation.required", { label: "${label}" })
}, }
}} }}
> >
<GlobalLoadingBar /> <GlobalLoadingBar />

View File

@@ -17,47 +17,32 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions"; import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors"; import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
selectBodyshop,
selectCurrentEula,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute"; import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/betaHandler"; import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component"; import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr"; import InstanceRenderMgr from "../utils/instanceRenderMgr";
import { ProductFruits } from 'react-product-fruits'; import { ProductFruits } from "react-product-fruits";
const ResetPassword = lazy(() => const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentEula: selectCurrentEula, currentEula: selectCurrentEula
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)), setOnline: (isOnline) => dispatch(setOnline(isOnline))
}); });
export function App({ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
currentEula,
}) {
const client = useSplitClient().client; const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false); const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -105,7 +90,7 @@ export function App({
window.location.hostname === window.location.hostname ===
InstanceRenderMgr({ InstanceRenderMgr({
imex: "beta.imex.online", imex: "beta.imex.online",
rome: "beta.romeonline.io", rome: "beta.romeonline.io"
}) })
) { ) {
console.log("LR Start"); console.log("LR Start");
@@ -113,7 +98,7 @@ export function App({
InstanceRenderMgr({ InstanceRenderMgr({
imex: "gvfvfw/bodyshopapp", imex: "gvfvfw/bodyshopapp",
rome: "rome-online/rome-online", rome: "rome-online/rome-online",
promanager: "", //TODO:AIO Add in log rocket for promanager instances. promanager: "" //TODO:AIO Add in log rocket for promanager instances.
}) })
); );
} }
@@ -151,7 +136,6 @@ export function App({
// Any route that is not assigned and matched will default to the Landing Page component // Any route that is not assigned and matched will default to the Landing Page component
return ( return (
<Suspense <Suspense
fallback={ fallback={
<LoadingSpinner <LoadingSpinner
@@ -164,13 +148,18 @@ export function App({
} }
> >
<ProductFruits <ProductFruits
workspaceCode={InstanceRenderMgr({imex:null, rome: null, promanager:"aoJoEifvezYI0Z0P"})} workspaceCode={InstanceRenderMgr({
imex: null,
rome: null,
promanager: "aoJoEifvezYI0Z0P"
})}
debug debug
language="en"
language="en" user={{ user={{
email: currentUser.email, email: currentUser.email,
username: currentUser.email, username: currentUser.email
}} /> }}
/>
<Routes> <Routes>
<Route <Route
@@ -225,9 +214,7 @@ export function App({
path="/manage/*" path="/manage/*"
element={ element={
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute <PrivateRoute isAuthorized={currentUser.authorized} />
isAuthorized={currentUser.authorized}
/>
</ErrorBoundary> </ErrorBoundary>
} }
> >
@@ -237,20 +224,13 @@ export function App({
path="/tech/*" path="/tech/*"
element={ element={
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute <PrivateRoute isAuthorized={currentUser.authorized} />
isAuthorized={currentUser.authorized}
/>
</ErrorBoundary> </ErrorBoundary>
} }
> >
<Route path="*" element={<TechPageContainer />} /> <Route path="*" element={<TechPageContainer />} />
</Route> </Route>
<Route <Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
path="/edit/*"
element={
<PrivateRoute isAuthorized={currentUser.authorized} />
}
>
<Route path="*" element={<DocumentEditorContainer />} /> <Route path="*" element={<DocumentEditorContainer />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -154,7 +154,6 @@
font-style: italic; font-style: italic;
} }
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4; background-color: #f4f4f4;
} }

View File

@@ -1,6 +1,6 @@
import { defaultsDeep } from "lodash"; import { defaultsDeep } from "lodash";
import { theme } from "antd"; import { theme } from "antd";
import InstanceRenderMgr from '../utils/instanceRenderMgr' import InstanceRenderMgr from "../utils/instanceRenderMgr";
const { defaultAlgorithm, darkAlgorithm } = theme; const { defaultAlgorithm, darkAlgorithm } = theme;
@@ -13,28 +13,28 @@ let isDarkMode = false;
const defaultTheme = { const defaultTheme = {
components: { components: {
Table: { Table: {
rowHoverBg: '#e7f3ff', rowHoverBg: "#e7f3ff",
rowSelectedBg: '#e6f7ff', rowSelectedBg: "#e6f7ff",
headerSortHoverBg: 'transparent', headerSortHoverBg: "transparent"
}, },
Menu: { Menu: {
darkItemHoverBg: '#1890ff', darkItemHoverBg: "#1890ff",
itemHoverBg: '#1890ff', itemHoverBg: "#1890ff",
horizontalItemHoverBg: '#1890ff', horizontalItemHoverBg: "#1890ff"
}, }
}, },
token: { token: {
colorPrimary: InstanceRenderMgr({ colorPrimary: InstanceRenderMgr({
imex: '#1890ff', imex: "#1890ff",
rome: '#326ade', rome: "#326ade",
promanager: "#1d69a6" promanager: "#1d69a6"
}), }),
colorInfo: InstanceRenderMgr({ colorInfo: InstanceRenderMgr({
imex: '#1890ff', imex: "#1890ff",
rome: '#326ade', rome: "#326ade",
promanager: "#1d69a6" promanager: "#1d69a6"
}), })
}, }
}; };
/** /**
@@ -44,13 +44,13 @@ const defaultTheme = {
const devTheme = { const devTheme = {
components: { components: {
Menu: { Menu: {
darkItemHoverBg: '#a51d1d', darkItemHoverBg: "#a51d1d",
itemHoverBg: '#a51d1d', itemHoverBg: "#a51d1d",
horizontalItemHoverBg: '#a51d1d', horizontalItemHoverBg: "#a51d1d"
} }
}, },
token: { token: {
colorPrimary: '#a51d1d' colorPrimary: "#a51d1d"
} }
}; };
@@ -60,11 +60,10 @@ const devTheme = {
*/ */
const prodTheme = {}; const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
: prodTheme;
const finaltheme = { const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme) ...defaultsDeep(currentTheme, defaultTheme)
} };
export default finaltheme; export default finaltheme;

View File

@@ -7,8 +7,7 @@ import {setModalContext} from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setRefundPaymentContext: (context) => setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
dispatch(setModalContext({context: context, modal: "refund_payment"})),
}); });
function Test({ setRefundPaymentContext, refundPaymentModal }) { function Test({ setRefundPaymentContext, refundPaymentModal }) {
@@ -18,7 +17,7 @@ function Test({setRefundPaymentContext, refundPaymentModal}) {
<Button <Button
onClick={() => onClick={() =>
setRefundPaymentContext({ setRefundPaymentContext({
context: {}, context: {}
}) })
} }
> >

View File

@@ -1,4 +1,5 @@
import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string"; import { Card, Checkbox, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -17,30 +18,22 @@ import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
bodyshop,
loading,
bills,
refetch,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]); const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -53,51 +46,45 @@ export function AccountingPayablesTableComponent({
dataIndex: "vendorname", dataIndex: "vendorname",
key: "vendorname", key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={{ to={{
pathname: `/manage/shop/vendors`, pathname: `/manage/shop/vendors`,
search: queryString.stringify({selectedvendor: record.vendor.id}), search: queryString.stringify({ selectedvendor: record.vendor.id })
}} }}
> >
{record.vendor.name} {record.vendor.name}
</Link> </Link>
), )
}, },
{ {
title: t("bills.fields.invoice_number"), title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number", dataIndex: "invoice_number",
key: "invoice_number", key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={{ to={{
pathname: `/manage/bills`, pathname: `/manage/bills`,
search: queryString.stringify({ search: queryString.stringify({
billid: record.id, billid: record.id,
vendorid: record.vendor.id, vendorid: record.vendor.id
}), })
}} }}
> >
{record.invoice_number} {record.invoice_number}
</Link> </Link>
), )
}, },
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
),
}, },
{ {
title: t("bills.fields.date"), title: t("bills.fields.date"),
@@ -105,9 +92,8 @@ export function AccountingPayablesTableComponent({
key: "date", key: "date",
sorter: (a, b) => dateSort(a.date, b.date), sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
state.sortedInfo.columnKey === "date" && state.sortedInfo.order, render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
}, },
{ {
title: t("bills.fields.total"), title: t("bills.fields.total"),
@@ -115,39 +101,29 @@ export function AccountingPayablesTableComponent({
key: "total", key: "total",
sorter: (a, b) => a.total - b.total, sorter: (a, b) => a.total - b.total,
sortOrder: sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
state.sortedInfo.columnKey === "total" && state.sortedInfo.order, render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
render: (text, record) => (
<CurrencyFormatter>{record.total}</CurrencyFormatter>
),
}, },
{ {
title: t("bills.fields.is_credit_memo"), title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo", dataIndex: "is_credit_memo",
key: "is_credit_memo", key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
state.sortedInfo.columnKey === "is_credit_memo" && render: (text, record) => <Checkbox disabled checked={record.is_credit_memo} />
state.sortedInfo.order,
render: (text, record) => (
<Checkbox disabled checked={record.is_credit_memo}/>
),
}, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
<ExportLogsCountDisplay logs={record.exportlogs}/>
),
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<PayableExportButton <PayableExportButton
billId={record.id} billId={record.id}
@@ -156,8 +132,8 @@ export function AccountingPayablesTableComponent({
setSelectedBills={setSelectedBills} setSelectedBills={setSelectedBills}
refetch={refetch} refetch={refetch}
/> />
), )
}, }
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
@@ -168,12 +144,8 @@ export function AccountingPayablesTableComponent({
const dataSource = state.search const dataSource = state.search
? bills.filter( ? bills.filter(
(v) => (v) =>
(v.vendor.name || "") (v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() (v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase())
.includes(state.search.toLowerCase()) ||
(v.invoice_number || "")
.toLowerCase()
.includes(state.search.toLowerCase())
) )
: bills; : bills;
@@ -195,15 +167,8 @@ export function AccountingPayablesTableComponent({
completedCallback={setSelectedBills} completedCallback={setSelectedBills}
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<QboAuthorizeComponent/> <Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
)}
<Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</Space> </Space>
} }
> >
@@ -215,16 +180,15 @@ export function AccountingPayablesTableComponent({
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
setSelectedBills(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedBills(selectedRows.map((i) => i.id)); setSelectedBills(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({
disabled: record.exported, disabled: record.exported
}), }),
selectedRowKeys: selectedBills, selectedRowKeys: selectedBills,
type: "checkbox", type: "checkbox"
}} }}
/> />
</Card> </Card>

View File

@@ -11,39 +11,29 @@ import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config"; 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 ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.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 PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
bodyshop,
loading,
payments,
refetch,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]); const [selectedPayments, setSelectedPayments] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -56,20 +46,16 @@ export function AccountingPayablesTableComponent({
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => <Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
),
}, },
{ {
title: t("payments.fields.date"), title: t("payments.fields.date"),
dataIndex: "date", dataIndex: "date",
key: "date", key: "date",
sorter: (a, b) => dateSort(a.date, b.date), sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
state.sortedInfo.columnKey === "date" && state.sortedInfo.order, render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
}, },
{ {
@@ -77,13 +63,8 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
alphaSort( sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.job.owner ? ( return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}> <Link to={"/manage/owners/" + record.job.owner.id}>
@@ -94,37 +75,33 @@ export function AccountingPayablesTableComponent({
<OwnerNameDisplay ownerObject={record.job} /> <OwnerNameDisplay ownerObject={record.job} />
</span> </span>
); );
}, }
}, },
{ {
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
sorter: (a, b) => a.amount - b.amount, sorter: (a, b) => a.amount - b.amount,
sortOrder: sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => ( render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
),
}, },
{ {
title: t("payments.fields.memo"), title: t("payments.fields.memo"),
dataIndex: "memo", dataIndex: "memo",
key: "memo", key: "memo"
}, },
{ {
title: t("payments.fields.transactionid"), title: t("payments.fields.transactionid"),
dataIndex: "transactionid", dataIndex: "transactionid",
key: "transactionid", key: "transactionid"
}, },
{ {
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at), key: "created_at",
sortOrder: sorter: (a, b) => dateSort(a.created_at, b.created_at),
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
}, },
//{ //{
// title: t("payments.fields.exportedat"), // title: t("payments.fields.exportedat"),
@@ -139,16 +116,13 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
<ExportLogsCountDisplay logs={record.exportlogs}/>
),
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}
@@ -157,8 +131,8 @@ export function AccountingPayablesTableComponent({
setSelectedPayments={setSelectedPayments} setSelectedPayments={setSelectedPayments}
refetch={refetch} refetch={refetch}
/> />
), )
}, }
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
@@ -169,21 +143,11 @@ export function AccountingPayablesTableComponent({
const dataSource = state.search const dataSource = state.search
? payments.filter( ? payments.filter(
(v) => (v) =>
(v.paymentnum || "") (v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() ((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
.includes(state.search.toLowerCase()) || ((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) ||
((v.job && v.job.ro_number) || "") ((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() ((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.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; : payments;
@@ -205,15 +169,8 @@ export function AccountingPayablesTableComponent({
completedCallback={setSelectedPayments} completedCallback={setSelectedPayments}
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<QboAuthorizeComponent/> <Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
)}
<Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</Space> </Space>
} }
> >
@@ -225,16 +182,15 @@ export function AccountingPayablesTableComponent({
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
setSelectedPayments(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedPayments(selectedRows.map((i) => i.id)); setSelectedPayments(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({
disabled: record.exported, disabled: record.exported
}), }),
selectedRowKeys: selectedPayments, selectedRowKeys: selectedPayments,
type: "checkbox", type: "checkbox"
}} }}
/> />
</Card> </Card>

View File

@@ -13,35 +13,25 @@ import {createStructuredSelector} from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
mapStateToProps,
mapDispatchToProps
)(AccountingReceivablesTableComponent);
export function AccountingReceivablesTableComponent({ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
bodyshop,
loading,
jobs,
refetch,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -54,11 +44,8 @@ export function AccountingReceivablesTableComponent({
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => <Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
),
}, },
{ {
@@ -66,29 +53,22 @@ export function AccountingReceivablesTableComponent({
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.date_invoiced"), title: t("jobs.fields.date_invoiced"),
dataIndex: "date_invoiced", dataIndex: "date_invoiced",
key: "date_invoiced", key: "date_invoiced",
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced), sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
sortOrder: sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
state.sortedInfo.columnKey === "date_invoiced" && render: (text, record) => <DateFormatter>{record.date_invoiced}</DateFormatter>
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.date_invoiced}</DateFormatter>
),
}, },
{ {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) => sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.owner ? ( return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}> <Link to={"/manage/owners/" + record.owner.id}>
@@ -99,7 +79,7 @@ export function AccountingReceivablesTableComponent({
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
</span> </span>
); );
}, }
}, },
{ {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
@@ -108,25 +88,19 @@ export function AccountingReceivablesTableComponent({
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
), ),
sortOrder: sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}> <Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
record.v_model_desc || ""
}`}
</Link> </Link>
) : ( ) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ <span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
record.v_model_desc || ""
}`}</span>
); );
}, }
}, },
{ {
title: t("jobs.fields.clm_no"), title: t("jobs.fields.clm_no"),
@@ -134,27 +108,23 @@ export function AccountingReceivablesTableComponent({
key: "clm_no", key: "clm_no",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.clm_total"), title: t("jobs.fields.clm_total"),
dataIndex: "clm_total", dataIndex: "clm_total",
key: "clm_total", key: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total, sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>; return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
}, }
}, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
<ExportLogsCountDisplay logs={record.exportlogs}/>
),
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -173,8 +143,8 @@ export function AccountingReceivablesTableComponent({
<Button>{t("jobs.labels.viewallocations")}</Button> <Button>{t("jobs.labels.viewallocations")}</Button>
</Link> </Link>
</Space> </Space>
), )
}, }
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
@@ -185,25 +155,12 @@ export function AccountingReceivablesTableComponent({
const dataSource = state.search const dataSource = state.search
? jobs.filter( ? jobs.filter(
(v) => (v) =>
(v.ro_number || "") (v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
.toString() (v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() (v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
.includes(state.search.toLowerCase()) || (v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.ownr_fn || "") (v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase() (v.v_make_desc || "").toLowerCase().includes(state.search.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()) (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
) )
: jobs; : jobs;
@@ -221,9 +178,7 @@ export function AccountingReceivablesTableComponent({
refetch={refetch} refetch={refetch}
/> />
)} )}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<QboAuthorizeComponent/>
)}
<Input.Search <Input.Search
value={state.search} value={state.search}
onChange={handleSearch} onChange={handleSearch}
@@ -241,16 +196,15 @@ export function AccountingReceivablesTableComponent({
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
setSelectedJobs(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedJobs(selectedRows.map((i) => i.id)); setSelectedJobs(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({
disabled: record.exported, disabled: record.exported
}), }),
selectedRowKeys: selectedJobs, selectedRowKeys: selectedJobs,
type: "checkbox", type: "checkbox"
}} }}
/> />
</Card> </Card>

View File

@@ -7,7 +7,7 @@ describe("Alert component", () => {
beforeEach(() => { beforeEach(() => {
const mockProps = { const mockProps = {
type: "error", type: "error",
message: "Test error message.", message: "Test error message."
}; };
wrapper = shallow(<Alert {...mockProps} />); wrapper = shallow(<Alert {...mockProps} />);

View File

@@ -19,7 +19,7 @@ export function AllocationsAssignmentComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const onChange = e => { const onChange = (e) => {
setAssignment({ ...assignment, employeeid: e }); setAssignment({ ...assignment, employeeid: e });
}; };
@@ -27,16 +27,16 @@ export function AllocationsAssignmentComponent({
const popContent = ( const popContent = (
<div> <div>
<Select id="employeeSelector" <Select
id="employeeSelector"
showSearch showSearch
style={{ width: 200 }} style={{ width: 200 }}
placeholder='Select a person' placeholder="Select a person"
optionFilterProp='children' optionFilterProp="children"
onChange={onChange} onChange={onChange}
filterOption={(input, option) => filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 >
}> {bodyshop.employees.map((emp) => (
{bodyshop.employees.map(emp => (
<Select.Option value={emp.id} key={emp.id}> <Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`} {`${emp.first_name} ${emp.last_name}`}
</Select.Option> </Select.Option>
@@ -47,13 +47,10 @@ export function AllocationsAssignmentComponent({
placeholder={t("joblines.fields.mod_lb_hrs")} placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)} max={parseFloat(maxHours)}
min={0} min={0}
onChange={e => setAssignment({...assignment, hours: e})} onChange={(e) => setAssignment({ ...assignment, hours: e })}
/> />
<Button <Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
type='primary'
disabled={!assignment.employeeid}
onClick={handleAssignment}>
Assign Assign
</Button> </Button>
<Button onClick={() => setVisibility(false)}>Close</Button> <Button onClick={() => setVisibility(false)}>Close</Button>
@@ -62,9 +59,7 @@ export function AllocationsAssignmentComponent({
return ( return (
<Popover content={popContent} open={visibility}> <Popover content={popContent} open={visibility}>
<Button onClick={() => setVisibility(true)}> <Button onClick={() => setVisibility(true)}>{t("allocations.actions.assign")}</Button>
{t("allocations.actions.assign")}
</Button>
</Popover> </Popover>
); );
} }

View File

@@ -13,7 +13,7 @@ describe("AllocationsAssignmentComponent component", () => {
assignment: {}, assignment: {},
setAssignment: jest.fn(), setAssignment: jest.fn(),
visibilityState: [false, jest.fn()], visibilityState: [false, jest.fn()],
maxHours: 4, maxHours: 4
}; };
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />); wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);

View File

@@ -5,17 +5,13 @@ import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { notification } from "antd"; import { notification } from "antd";
export default function AllocationsAssignmentContainer({ export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
jobLineId,
hours,
refetch,
}) {
const visibilityState = useState(false); const visibilityState = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [assignment, setAssignment] = useState({ const [assignment, setAssignment] = useState({
joblineid: jobLineId, joblineid: jobLineId,
hours: parseFloat(hours), hours: parseFloat(hours),
employeeid: null, employeeid: null
}); });
const [insertAllocation] = useMutation(INSERT_ALLOCATION); const [insertAllocation] = useMutation(INSERT_ALLOCATION);
@@ -23,14 +19,14 @@ export default function AllocationsAssignmentContainer({
insertAllocation({ variables: { alloc: { ...assignment } } }) insertAllocation({ variables: { alloc: { ...assignment } } })
.then((r) => { .then((r) => {
notification["success"]({ notification["success"]({
message: t("allocations.successes.save"), message: t("allocations.successes.save")
}); });
visibilityState[1](false); visibilityState[1](false);
if (refetch) refetch(); if (refetch) refetch();
}) })
.catch((error) => { .catch((error) => {
notification["error"]({ notification["error"]({
message: t("employees.errors.saving", {message: error.message}), message: t("employees.errors.saving", { message: error.message })
}); });
}); });
}; };

View File

@@ -6,7 +6,7 @@ import {createStructuredSelector} from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export default connect( export default connect(
@@ -18,7 +18,7 @@ export default connect(
handleAssignment, handleAssignment,
assignment, assignment,
setAssignment, setAssignment,
visibilityState, visibilityState
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -36,9 +36,7 @@ export default connect(
placeholder="Select a person" placeholder="Select a person"
optionFilterProp="children" optionFilterProp="children"
onChange={onChange} onChange={onChange}
filterOption={(input, option) => filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
> >
{bodyshop.employees.map((emp) => ( {bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}> <Select.Option value={emp.id} key={emp.id}>
@@ -47,11 +45,7 @@ export default connect(
))} ))}
</Select> </Select>
<Button <Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
type="primary"
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
Assign Assign
</Button> </Button>
<Button onClick={() => setVisibility(false)}>Close</Button> <Button onClick={() => setVisibility(false)}>Close</Button>

View File

@@ -5,14 +5,11 @@ import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { notification } from "antd"; import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({ export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
jobLines,
refetch,
}) {
const visibilityState = useState(false); const visibilityState = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [assignment, setAssignment] = useState({ const [assignment, setAssignment] = useState({
employeeid: null, employeeid: null
}); });
const [insertAllocation] = useMutation(INSERT_ALLOCATION); const [insertAllocation] = useMutation(INSERT_ALLOCATION);
@@ -21,14 +18,14 @@ export default function AllocationsBulkAssignmentContainer({
acc.push({ acc.push({
joblineid: value.id, joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0, hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid, employeeid: assignment.employeeid
}); });
return acc; return acc;
}, []); }, []);
insertAllocation({ variables: { alloc: allocs } }).then((r) => { insertAllocation({ variables: { alloc: allocs } }).then((r) => {
notification["success"]({ notification["success"]({
message: t("employees.successes.save"), message: t("employees.successes.save")
}); });
visibilityState[1](false); visibilityState[1](false);
if (refetch) refetch(); if (refetch) refetch();

View File

@@ -6,15 +6,9 @@ export default function AllocationsLabelComponent({allocation, handleClick}) {
return ( return (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<span> <span>
{`${allocation.employee.first_name || ""} ${ {`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
allocation.employee.last_name || ""
} (${allocation.hours || ""})`}
</span> </span>
<Icon <Icon style={{ color: "red", padding: "0px 4px" }} component={MdRemoveCircleOutline} onClick={handleClick} />
style={{color: "red", padding: "0px 4px"}}
component={MdRemoveCircleOutline}
onClick={handleClick}
/>
</div> </div>
); );
} }

View File

@@ -14,7 +14,7 @@ export default function AllocationsLabelContainer({allocation, refetch}) {
deleteAllocation({ variables: { id: allocation.id } }) deleteAllocation({ variables: { id: allocation.id } })
.then((r) => { .then((r) => {
notification["success"]({ notification["success"]({
message: t("allocations.successes.deleted"), message: t("allocations.successes.deleted")
}); });
if (refetch) refetch(); if (refetch) refetch();
}) })
@@ -23,10 +23,5 @@ export default function AllocationsLabelContainer({allocation, refetch}) {
}); });
}; };
return ( return <AllocationsLabelComponent allocation={allocation} handleClick={handleClick} />;
<AllocationsLabelComponent
allocation={allocation}
handleClick={handleClick}
/>
);
} }

View File

@@ -9,7 +9,7 @@ import {pageLimit} from "../../utils/config";
export default function AuditTrailListComponent({ loading, data }) { export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {}
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -18,12 +18,9 @@ export default function AuditTrailListComponent({loading, data}) {
dataIndex: " created", dataIndex: " created",
key: " created", key: " created",
width: "10%", width: "10%",
render: (text, record) => ( render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created, sorter: (a, b) => a.created - b.created,
sortOrder: sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
}, },
{ {
title: t("audit.fields.operation"), title: t("audit.fields.operation"),
@@ -31,20 +28,14 @@ export default function AuditTrailListComponent({loading, data}) {
key: "operation", key: "operation",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.operation, b.operation), sorter: (a, b) => alphaSort(a.operation, b.operation),
sortOrder: sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
}, },
{ {
title: t("audit.fields.values"), title: t("audit.fields.values"),
dataIndex: " old_val", dataIndex: " old_val",
key: " old_val", key: " old_val",
width: "10%", width: "10%",
render: (text, record) => ( render: (text, record) => <AuditTrailValuesComponent oldV={record.old_val} newV={record.new_val} />
<AuditTrailValuesComponent
oldV={record.old_val}
newV={record.new_val}
/>
),
}, },
{ {
title: t("audit.fields.useremail"), title: t("audit.fields.useremail"),
@@ -52,20 +43,19 @@ export default function AuditTrailListComponent({loading, data}) {
key: "useremail", key: "useremail",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail), sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder: sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order, }
},
]; ];
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: { span: 12 }, xs: { span: 12 },
sm: {span: 5}, sm: { span: 5 }
}, },
wrapperCol: { wrapperCol: {
xs: { span: 24 }, xs: { span: 24 },
sm: {span: 12}, sm: { span: 12 }
}, }
}; };
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });

View File

@@ -11,7 +11,7 @@ export default function AuditTrailListContainer({recordId}) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, { const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { id: recordId }, variables: { id: recordId },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
logImEXEvent("audittrail_view", { recordId }); logImEXEvent("audittrail_view", { recordId });
@@ -22,16 +22,10 @@ export default function AuditTrailListContainer({recordId}) {
) : ( ) : (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Card> <Card>
<AuditTrailListComponent <AuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card> </Card>
<Card> <Card>
<EmailAuditTrailListComponent <EmailAuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card> </Card>
</Row> </Row>
)} )}

View File

@@ -8,7 +8,7 @@ import {pageLimit} from "../../utils/config";
export default function EmailAuditTrailListComponent({ loading, data }) { export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {}
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -17,12 +17,9 @@ export default function EmailAuditTrailListComponent({loading, data}) {
dataIndex: " created", dataIndex: " created",
key: " created", key: " created",
width: "10%", width: "10%",
render: (text, record) => ( render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created, sorter: (a, b) => a.created - b.created,
sortOrder: sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
}, },
{ {
@@ -31,20 +28,19 @@ export default function EmailAuditTrailListComponent({loading, data}) {
key: "useremail", key: "useremail",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail), sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder: sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order, }
},
]; ];
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: { span: 12 }, xs: { span: 12 },
sm: {span: 5}, sm: { span: 5 }
}, },
wrapperCol: { wrapperCol: {
xs: { span: 24 }, xs: { span: 24 },
sm: {span: 12}, sm: { span: 12 }
}, }
}; };
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });

View File

@@ -8,7 +8,7 @@ export default function AuditTrailValuesComponent({oldV, newV}) {
if (!oldV && newV) if (!oldV && newV)
return ( return (
<List style={{width: "800px"}} bordered size='small'> <List style={{ width: "800px" }} bordered size="small">
{Object.keys(newV).map((key, idx) => ( {Object.keys(newV).map((key, idx) => (
<List.Item key={idx} value={key}> <List.Item key={idx} value={key}>
{key}: {JSON.stringify(newV[key])} {key}: {JSON.stringify(newV[key])}
@@ -18,7 +18,7 @@ export default function AuditTrailValuesComponent({oldV, newV}) {
); );
return ( return (
<List style={{width: "800px"}} bordered size='small'> <List style={{ width: "800px" }} bordered size="small">
{Object.keys(oldV).map((key, idx) => ( {Object.keys(oldV).map((key, idx) => (
<List.Item key={idx}> <List.Item key={idx}>
{key}: {oldV[key]} <Icon component={FaArrowRight} /> {key}: {oldV[key]} <Icon component={FaArrowRight} />

View File

@@ -7,15 +7,7 @@ export default function BarcodePopupComponent({value, children}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<Popover <Popover content={<Barcode value={value || ""} background="transparent" displayValue={false} />}>
content={
<Barcode
value={value || ""}
background="transparent"
displayValue={false}
/>
}
>
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>} {children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
</Popover> </Popover>
</div> </div>

View File

@@ -4,17 +4,13 @@ import {useTranslation} from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss"; import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({ export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
form,
returnLoading,
returnData,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (returnData) { if (returnData) {
form.setFieldsValue({ form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines, outstanding_returns: returnData.parts_order_lines
}); });
} }
}, [returnData, form]); }, [returnData, form]);
@@ -22,9 +18,7 @@ export default function BillCmdReturnsTableComponent({
return ( return (
<Form.Item <Form.Item
shouldUpdate={(prev, cur) => shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid || prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
} }
noStyle noStyle
> >
@@ -42,9 +36,7 @@ export default function BillCmdReturnsTableComponent({
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
<> <>
<Typography.Title level={4}> <Typography.Title level={4}>{t("bills.labels.creditsnotreceived")}</Typography.Title>
{t("bills.labels.creditsnotreceived")}
</Typography.Title>
<table className="bill-cm-returns-table"> <table className="bill-cm-returns-table">
<thead> <thead>
<tr> <tr>

View File

@@ -12,8 +12,7 @@ import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton); export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
@@ -32,18 +31,14 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
cache.modify({ cache.modify({
fields: { fields: {
bills(existingBills, { readField }) { bills(existingBills, { readField }) {
return existingBills.filter( return existingBills.filter((billref) => bill.id !== readField("id", billref));
(billref) => bill.id !== readField("id", billref)
);
}, },
search_bills(existingBills, { readField }) { search_bills(existingBills, { readField }) {
return existingBills.filter( return existingBills.filter((billref) => bill.id !== readField("id", billref));
(billref) => bill.id !== readField("id", billref) }
); }
},
},
}); });
}, }
}); });
if (!!!result.errors) { if (!!!result.errors) {
@@ -51,7 +46,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: jobid, jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number), operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted", type: "billdeleted"
}); });
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
@@ -62,14 +57,14 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
if (error.toLowerCase().includes("inventory_billid_fkey")) { if (error.toLowerCase().includes("inventory_billid_fkey")) {
notification["error"]({ notification["error"]({
message: t("bills.errors.deleting", { message: t("bills.errors.deleting", {
error: t("bills.errors.existinginventoryline"), error: t("bills.errors.existinginventoryline")
}), })
}); });
} else { } else {
notification["error"]({ notification["error"]({
message: t("bills.errors.deleting", { message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors)
}), })
}); });
} }
} }
@@ -79,11 +74,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
return ( return (
<RbacWrapper action="bills:delete" noauth={<></>}> <RbacWrapper action="bills:delete" noauth={<></>}>
<Popconfirm <Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
disabled={bill.exported}
onConfirm={handleDelete}
title={t("bills.labels.deleteconfirm")}
>
<Button <Button
disabled={bill.exported} disabled={bill.exported}
// onClick={handleDelete} // onClick={handleDelete}

View File

@@ -25,21 +25,16 @@ 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
dispatch(setModalContext({context: context, modal: "partsOrder"})), insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) { export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -55,7 +50,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
variables: { billid: search.billid }, variables: { billid: search.billid },
skip: !!!search.billid, skip: !!!search.billid,
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
// ... rest of the code remains the same // ... rest of the code remains the same
@@ -64,8 +59,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
//It's got a previously deducted bill line! //It's got a previously deducted bill line!
if ( if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0
0
) )
setOpen(true); setOpen(true);
else { else {
@@ -81,7 +75,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
const updates = []; const updates = [];
updates.push( updates.push(
update_bill({ update_bill({
variables: {billId: search.billid, bill: bill}, variables: { billId: search.billid, bill: bill }
}) })
); );
@@ -115,9 +109,9 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
billLine: { billLine: {
...il, ...il,
deductedfromlbr: deductedfromlbr, deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid, joblineid: il.joblineid === "noline" ? null : il.joblineid
}, }
}, }
}) })
); );
} else { } else {
@@ -130,10 +124,10 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
...il, ...il,
deductedfromlbr: deductedfromlbr, deductedfromlbr: deductedfromlbr,
billid: search.billid, billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid, joblineid: il.joblineid === "noline" ? null : il.joblineid
}, }
], ]
}, }
}) })
); );
} }
@@ -145,7 +139,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
jobid: bill.jobid, jobid: bill.jobid,
billid: search.billid, billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number), operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated", type: "billupdated"
}); });
await refetch(); await refetch();
@@ -166,10 +160,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
{data && ( {data && (
<> <>
<PageHeader <PageHeader
title={ title={data && `${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`}
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={ extra={
<Space> <Space>
<BillDetailEditReturn data={data} /> <BillDetailEditReturn data={data} />
@@ -196,12 +187,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
</Space> </Space>
} }
/> />
<Form <Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} /> <BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider> <Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? ( {bodyshop.uselocalmediaserver ? (
@@ -235,14 +221,13 @@ const transformData = (data) => {
...i, ...i,
joblineid: !!i.joblineid ? i.joblineid : "noline", joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: { applicable_taxes: {
federal: federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false, state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false, local: (i.applicable_taxes && i.applicable_taxes.local) || false
}, }
}; };
}), }),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null, date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null
} }
: {}; : {};
}; };

View File

@@ -11,27 +11,16 @@ import {selectBodyshop} from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
dispatch(setModalContext({context: context, modal: "partsOrder"})), insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -59,11 +48,11 @@ export function BillDetailEditReturn({
quantity: i.quantity, quantity: i.quantity,
joblineid: i.joblineid, joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno, oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type, part_type: i.jobline && i.jobline.part_type
}; };
}), }),
isReturn: true, isReturn: true
}, }
}); });
delete search.billid; delete search.billid;
@@ -83,11 +72,7 @@ export function BillDetailEditReturn({
title={t("bills.actions.return")} title={t("bills.actions.return")}
onOk={() => form.submit()} onOk={() => form.submit()}
> >
<Form <Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}> <Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -98,12 +83,10 @@ export function BillDetailEditReturn({
<Checkbox <Checkbox
onChange={(e) => { onChange={(e) => {
form.setFieldsValue({ form.setFieldsValue({
billlines: form billlines: form.getFieldsValue().billlines.map((b) => ({
.getFieldsValue()
.billlines.map((b) => ({
...b, ...b,
selected: e.target.checked, selected: e.target.checked
})), }))
}); });
}} }}
/> />
@@ -173,11 +156,7 @@ export function BillDetailEditReturn({
</Form> </Form>
</Modal> </Modal>
<Button <Button
disabled={ disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
onClick={() => { onClick={() => {
setOpen(true); setOpen(true);
}} }}

View File

@@ -18,11 +18,9 @@ export default function BillDetailEditcontainer() {
md: "100%", md: "100%",
lg: "100%", lg: "100%",
xl: "90%", xl: "90%",
xxl: "90%", xxl: "90%"
}; };
const drawerPercentage = selectedBreakpoint const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
? bpoints[selectedBreakpoint[0]]
: "100%";
return ( return (
<Drawer <Drawer

View File

@@ -9,12 +9,12 @@ import {createStructuredSelector} from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries"; import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries"; import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries"; import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries"; import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
@@ -28,23 +28,17 @@ import {handleUpload} from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation, type }) => insertAuditTrail: ({ jobid, billid, operation, type }) =>
dispatch(insertAuditTrail({jobid, billid, operation, type })), dispatch(insertAuditTrail({ jobid, billid, operation, type }))
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
function BillEnterModalContainer({ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
billEnterModal,
toggleModalVisible,
bodyshop,
currentUser,
insertAuditTrail,
}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false); const [enterAgain, setEnterAgain] = useState(false);
@@ -54,15 +48,14 @@ function BillEnterModalContainer({
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const client = useApolloClient(); const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage( const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
"enter_bill_generate_label",
false
);
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({ const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {}, attributes: {},
names: ["Enhanced_Payroll"], names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid, splitKey: bodyshop.imexshopid
}); });
const formValues = useMemo(() => { const formValues = useMemo(() => {
@@ -71,19 +64,12 @@ function BillEnterModalContainer({
//Added as a part of IO-2436 for capturing parts price changes. //Added as a part of IO-2436 for capturing parts price changes.
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({ billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
...line, ...line,
original_actual_price: line.actual_price, original_actual_price: line.actual_price
})), })),
jobid: jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null,
(billEnterModal.context.job && billEnterModal.context.job.id) || null, federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0,
federal_tax_rate: state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0,
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0
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]); }, [billEnterModal, bodyshop]);
@@ -96,14 +82,7 @@ function BillEnterModalContainer({
} }
setLoading(true); setLoading(true);
const { const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
upload,
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;
let adjustmentsToInsert = {}; let adjustmentsToInsert = {};
let payrollAdjustmentsToInsert = []; let payrollAdjustmentsToInsert = [];
@@ -137,8 +116,8 @@ function BillEnterModalContainer({
convertedtolbr: true, convertedtolbr: true,
convertedtolbr_data: { convertedtolbr_data: {
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1, mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
mod_lbr_ty: lbr_adjustment.mod_lbr_ty, mod_lbr_ty: lbr_adjustment.mod_lbr_ty
}, }
}); });
} }
} else { } else {
@@ -155,21 +134,15 @@ function BillEnterModalContainer({
lbr_adjustment, lbr_adjustment,
joblineid: i.joblineid === "noline" ? null : i.joblineid, joblineid: i.joblineid === "noline" ? null : i.joblineid,
applicable_taxes: { applicable_taxes: {
federal: federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
(i.applicable_taxes && i.applicable_taxes.federal) || state: (i.applicable_taxes && i.applicable_taxes.state) || false,
false, local: (i.applicable_taxes && i.applicable_taxes.local) || 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"], refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
awaitRefetchQueries: true awaitRefetchQueries: true
@@ -182,9 +155,9 @@ function BillEnterModalContainer({
lineId: li.id, lineId: li.id,
line: { line: {
convertedtolbr: li.convertedtolbr, convertedtolbr: li.convertedtolbr,
convertedtolbr_data: li.convertedtolbr_data, convertedtolbr_data: li.convertedtolbr_data
}, }
}, }
}); });
}) })
); );
@@ -195,60 +168,56 @@ function BillEnterModalContainer({
const existingAdjustments = await client.query({ const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS, query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: { variables: {
id: values.jobid, id: values.jobid
}, }
}); });
const newAdjustments = _.cloneDeep( const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
adjKeys.forEach((key) => { adjKeys.forEach((key) => {
newAdjustments[key] = newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({ insertAuditTrail({
jobid: values.jobid, jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({ operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key, mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1), hours: adjustmentsToInsert[key].toFixed(1)
}), }),
type: "jobmodifylbradj",}); type: "jobmodifylbradj"
});
}); });
const jobUpdate = client.mutate({ const jobUpdate = client.mutate({
mutation: UPDATE_JOB, mutation: UPDATE_JOB,
variables: { variables: {
jobId: values.jobid, jobId: values.jobid,
job: {lbr_adjustments: newAdjustments}, job: { lbr_adjustments: newAdjustments }
}, }
}); });
if (!!jobUpdate.errors) { if (!!jobUpdate.errors) {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors), message: JSON.stringify(jobUpdate.errors)
}), })
}); });
return; return;
} }
} }
const markPolReceived = const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true);
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) { if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({ const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) }, variables: { partsLineIds: markPolReceived.map((p) => p.id) },
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID" ], refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
}); });
if (!!r2.errors) { if (!!r2.errors) {
setLoading(false); setLoading(false);
setEnterAgain(false); setEnterAgain(false);
notification["error"]({ notification["error"]({
message: t("parts_orders.errors.updating", { message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors), message: JSON.stringify(r2.errors)
}), })
}); });
} }
} }
@@ -258,29 +227,28 @@ function BillEnterModalContainer({
setEnterAgain(false); setEnterAgain(false);
notification["error"]({ notification["error"]({
message: t("bills.errors.creating", { message: t("bills.errors.creating", {
message: JSON.stringify(r1.errors), message: JSON.stringify(r1.errors)
}), })
}); });
} }
const billId = r1.data.insert_bills.returning[0].id; const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed = const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory);
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) { if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({ const r2 = await updateInventoryLines({
variables: { variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id), InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId, consumedbybillid: billId
}, }
}); });
if (!!r2.errors) { if (!!r2.errors) {
setLoading(false); setLoading(false);
setEnterAgain(false); setEnterAgain(false);
notification["error"]({ notification["error"]({
message: t("inventory.errors.updating", { message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors), message: JSON.stringify(r2.errors)
}), })
}); });
} }
} }
@@ -296,18 +264,16 @@ function BillEnterModalContainer({
lineId: li.joblineid, lineId: li.joblineid,
line: { line: {
location: li.location || location, location: li.location || location,
status: status: bodyshop.md_order_statuses.default_received || "Received*",
bodyshop.md_order_statuses.default_received || "Received*",
//Added parts price changes. //Added parts price changes.
...(li.create_ppc && ...(li.create_ppc && li.original_actual_price !== li.actual_price
li.original_actual_price !== li.actual_price
? { ? {
act_price_before_ppc: li.original_actual_price, act_price_before_ppc: li.original_actual_price,
act_price: li.actual_price, act_price: li.actual_price
}
: {})
}
} }
: {}),
},
},
}); });
}) })
); );
@@ -324,8 +290,8 @@ function BillEnterModalContainer({
context: { context: {
jobid: values.jobid, jobid: values.jobid,
invoice_number: remainingValues.invoice_number, invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid, vendorid: remainingValues.vendorid
}, }
}); });
}); });
} else { } else {
@@ -338,7 +304,7 @@ function BillEnterModalContainer({
jobId: values.jobid, jobId: values.jobid,
billId: billId, billId: billId,
tagsArray: null, tagsArray: null,
callback: null, callback: null
} }
); );
}); });
@@ -347,7 +313,7 @@ function BillEnterModalContainer({
/////////////////////////// ///////////////////////////
setLoading(false); setLoading(false);
notification["success"]({ notification["success"]({
message: t("bills.successes.created"), message: t("bills.successes.created")
}); });
if (generateLabel) { if (generateLabel) {
@@ -355,8 +321,8 @@ function BillEnterModalContainer({
{ {
name: Templates.parts_invoice_label_single.key, name: Templates.parts_invoice_label_single.key,
variables: { variables: {
id: billId, id: billId
}, }
}, },
{}, {},
"p" "p"
@@ -368,10 +334,8 @@ function BillEnterModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: values.jobid, jobid: values.jobid,
billid: billId, billid: billId,
operation: AuditTrailMapping.billposted( operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
r1.data.insert_bills.returning[0].invoice_number type: "billposted"
),
type: "billposted",
}); });
if (enterAgain) { if (enterAgain) {
@@ -379,7 +343,7 @@ function BillEnterModalContainer({
form.setFieldsValue({ form.setFieldsValue({
...formValues, ...formValues,
vendorid: values.vendorid, vendorid: values.vendorid,
billlines: [], billlines: []
}); });
// form.resetFields(); // form.resetFields();
} else { } else {
@@ -422,10 +386,7 @@ function BillEnterModalContainer({
}} }}
footer={ footer={
<Space> <Space>
<Checkbox <Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
checked={generateLabel}
onChange={(e) => setGenerateLabel(e.target.checked)}
>
{t("bills.labels.generatepartslabel")} {t("bills.labels.generatepartslabel")}
</Checkbox> </Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button> <Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
@@ -456,16 +417,10 @@ function BillEnterModalContainer({
setEnterAgain(false); setEnterAgain(false);
}} }}
> >
<BillFormContainer <BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
form={form}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</Form> </Form>
</Modal> </Modal>
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);
mapStateToProps,
mapDispatchToProps
)(BillEnterModalContainer);

View File

@@ -5,13 +5,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component"; import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({ export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
lineData,
discount,
form,
responsibilityCenters,
disabled,
}) {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -20,14 +14,14 @@ export default function BillFormLinesExtended({
dataIndex: "line_desc", dataIndex: "line_desc",
key: "line_desc", key: "line_desc",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), sorter: (a, b) => alphaSort(a.line_desc, b.line_desc)
}, },
{ {
title: t("joblines.fields.oem_partno"), title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno", dataIndex: "oem_partno",
key: "oem_partno", key: "oem_partno",
width: "10%", width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno), sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
}, },
{ {
title: t("joblines.fields.part_type"), title: t("joblines.fields.part_type"),
@@ -37,30 +31,27 @@ export default function BillFormLinesExtended({
filters: [ filters: [
{ {
text: t("jobs.labels.partsfilter"), text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"], value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
}, },
{ {
text: t("joblines.fields.part_types.PAN"), text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"], value: ["PAN", "PAP"]
}, },
{ {
text: t("joblines.fields.part_types.PAL"), text: t("joblines.fields.part_types.PAL"),
value: ["PAL"], value: ["PAL"]
}, },
{ {
text: t("joblines.fields.part_types.PAA"), text: t("joblines.fields.part_types.PAA"),
value: ["PAA"], value: ["PAA"]
}, },
{ {
text: t("joblines.fields.part_types.PAS"), text: t("joblines.fields.part_types.PAS"),
value: ["PAS", "PASL"], value: ["PAS", "PASL"]
}, }
], ],
onFilter: (value, record) => value.includes(record.part_type), onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) => render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
}, },
{ {
@@ -73,20 +64,16 @@ export default function BillFormLinesExtended({
render: (text, record) => ( render: (text, record) => (
<> <>
<CurrencyFormatter> <CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511" {record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter> </CurrencyFormatter>
{record.part_qty ? `(x ${record.part_qty})` : null} {record.part_qty ? `(x ${record.part_qty})` : null}
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span <span style={{ marginLeft: ".2rem" }}>{`(${record.prt_dsmk_p}%)`}</span>
style={{marginLeft: ".2rem"}}
>{`(${record.prt_dsmk_p}%)`}</span>
) : ( ) : (
<></> <></>
)} )}
</> </>
), )
}, },
{ {
title: t("billlines.fields.posting"), title: t("billlines.fields.posting"),
@@ -103,8 +90,8 @@ export default function BillFormLinesExtended({
discount={discount} discount={discount}
/> />
</Form.Item> </Form.Item>
), )
}, }
]; ];
const data = const data =
@@ -112,25 +99,16 @@ export default function BillFormLinesExtended({
? lineData ? lineData
: lineData.filter( : lineData.filter(
(l) => (l) =>
(l.line_desc && (l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
l.line_desc.toLowerCase().includes(search.toLowerCase())) || (l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.oem_partno && (l.act_price && l.act_price.toString().startsWith(search.toString()))
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.act_price &&
l.act_price.toString().startsWith(search.toString()))
); );
return ( return (
<Form.Item noStyle name="billlineskeys"> <Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button> <button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear /> <Input onChange={(e) => setSearch(e.target.value)} allowClear />
<Table <Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} />
pagination={false}
size="small"
columns={columns}
rowKey="id"
dataSource={data}
/>
</Form.Item> </Form.Item>
); );
} }

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons"; import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -10,15 +10,12 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component"
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({ export function BillFormItemsExtendedFormItem({
value, value,
@@ -28,7 +25,7 @@ export function BillFormItemsExtendedFormItem({
index, index,
disabled, disabled,
responsibilityCenters, responsibilityCenters,
discount, discount
}) { }) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys"); // const { billlineskeys } = form.getFieldsValue("billlineskeys");
@@ -51,12 +48,10 @@ export function BillFormItemsExtendedFormItem({
cost_center: record.part_type cost_center: record.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? record.part_type ? record.part_type
: responsibilityCenters.defaults && : responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
(responsibilityCenters.defaults.costs[record.part_type] || : null
null) }
: null, }
},
},
}); });
}} }}
> >
@@ -66,22 +61,13 @@ export function BillFormItemsExtendedFormItem({
return ( return (
<Space wrap> <Space wrap>
<Form.Item <Form.Item label={t("billlines.fields.line_desc")} name={["billlineskeys", record.id, "line_desc"]}>
label={t("billlines.fields.line_desc")}
name={["billlineskeys", record.id, "line_desc"]}
>
<Input disabled={disabled} /> <Input disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("billlines.fields.quantity")} name={["billlineskeys", record.id, "quantity"]}>
label={t("billlines.fields.quantity")}
name={["billlineskeys", record.id, "quantity"]}
>
<InputNumber precision={0} min={0} disabled={disabled} /> <InputNumber precision={0} min={0} disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("billlines.fields.actual_price")} name={["billlineskeys", record.id, "actual_price"]}>
label={t("billlines.fields.actual_price")}
name={["billlineskeys", record.id, "actual_price"]}
>
<CurrencyInput <CurrencyInput
min={0} min={0}
disabled={disabled} disabled={disabled}
@@ -94,52 +80,34 @@ export function BillFormItemsExtendedFormItem({
...billlineskeys[billlineskeys], ...billlineskeys[billlineskeys],
actual_cost: !!billlineskeys[billlineskeys].actual_cost actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost ? billlineskeys[billlineskeys].actual_cost
: Math.round( : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
(parseFloat(e.target.value) * (1 - discount) + }
Number.EPSILON) * }
100
) / 100,
},
},
}); });
}} }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("billlines.fields.actual_cost")} name={["billlineskeys", record.id, "actual_cost"]}>
label={t("billlines.fields.actual_cost")}
name={["billlineskeys", record.id, "actual_cost"]}
>
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
const line = value; const line = value;
if (!!!line) return null; if (!!!line) return null;
const lineDiscount = ( const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
if (lineDiscount - discount === 0) return <div />; if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />; return <WarningOutlined style={{ color: "red" }} />;
}} }}
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
label={t("billlines.fields.cost_center")}
name={["billlineskeys", record.id, "cost_center"]}
>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false) ? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ( : responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
label={t("billlines.fields.location")}
name={["billlineskeys", record.id, "location"]}
>
<Select disabled={disabled}> <Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
@@ -157,70 +125,34 @@ export function BillFormItemsExtendedFormItem({
</Form.Item> </Form.Item>
<Form.Item shouldUpdate style={{ display: "inline-block" }}> <Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => { {() => {
if ( if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr)
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
.deductedfromlbr
)
return ( return (
<div> <div>
<Form.Item <Form.Item
label={t("joblines.fields.mod_lbr_ty")} label={t("joblines.fields.mod_lbr_ty")}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]}
name={[
"billlineskeys",
record.id,
"lbr_adjustment",
"mod_lbr_ty",
]} ]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select allowClear> <Select allowClear>
<Select.Option value="LAA"> <Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
{t("joblines.fields.lbr_types.LAA")} <Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
</Select.Option> <Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAB"> <Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
{t("joblines.fields.lbr_types.LAB")} <Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
</Select.Option> <Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAD"> <Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
{t("joblines.fields.lbr_types.LAD")} <Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
</Select.Option> <Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAE"> <Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
{t("joblines.fields.lbr_types.LAE")} <Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
</Select.Option> <Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LAF"> <Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
{t("joblines.fields.lbr_types.LAF")} <Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -229,9 +161,9 @@ export function BillFormItemsExtendedFormItem({
initialValue={bodyshop.default_adjustment_rate} initialValue={bodyshop.default_adjustment_rate}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<InputNumber precision={2} min={0.01} /> <InputNumber precision={2} min={0.01} />
@@ -272,8 +204,8 @@ export function BillFormItemsExtendedFormItem({
...values, ...values,
billlineskeys: { billlineskeys: {
...(values.billlineskeys || {}), ...(values.billlineskeys || {}),
[record.id]: null, [record.id]: null
}, }
}); });
}} }}
> >

View File

@@ -1,30 +1,30 @@
import Icon, { UploadOutlined } from '@ant-design/icons'; import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from "@apollo/client";
import { useSplitTreatments } from '@splitsoftware/splitio-react'; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd'; import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { MdOpenInNew } from 'react-icons/md'; import { MdOpenInNew } from "react-icons/md";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { createStructuredSelector } from 'reselect'; import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries'; import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from '../../redux/user/user.selectors'; import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from '../../utils/day'; import dayjs from "../../utils/day";
import InstanceRenderManager from '../../utils/instanceRenderMgr'; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from '../alert/alert.component'; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component'; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from '../form-date-picker/form-date-picker.component'; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component'; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from '../form-items-formatted/currency-form-item.component'; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from '../job-search-select/job-search-select.component'; import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from '../layout-form-row/layout-form-row.component'; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component'; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from './bill-form.lines.component'; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from './bill-form.totals.utility'; import { CalculateBillTotal } from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
@@ -41,18 +41,18 @@ export function BillFormComponent({
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake, preferredMake
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const [discount, setDiscount] = useState(0); const [discount, setDiscount] = useState(0);
const { const {
treatments: { Extended_Bill_Posting, ClosingPeriod }, treatments: { Extended_Bill_Posting, ClosingPeriod }
} = useSplitTreatments({ } = useSplitTreatments({
attributes: {}, attributes: {},
names: ['Extended_Bill_Posting', 'ClosingPeriod'], names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid, splitKey: bodyshop.imexshopid
}); });
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
@@ -62,16 +62,16 @@ export function BillFormComponent({
!billEdit && !billEdit &&
loadOutstandingReturns({ loadOutstandingReturns({
variables: { variables: {
jobId: form.getFieldValue('jobid'), jobId: form.getFieldValue("jobid"),
vendorId: opt.value, vendorId: opt.value
}, }
}); });
}; };
const handleFederalTaxExemptSwitchToggle = (checked) => { const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate // Early gate
if (!checked) return; if (!checked) return;
const values = form.getFieldsValue('billlines'); const values = form.getFieldsValue("billlines");
// Gate bill lines // Gate bill lines
if (!values?.billlines?.length) return; if (!values?.billlines?.length) return;
@@ -83,26 +83,26 @@ export function BillFormComponent({
}; };
useEffect(() => { useEffect(() => {
if (job) form.validateFields(['is_credit_memo']); if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]); }, [job, form]);
useEffect(() => { useEffect(() => {
const vendorId = form.getFieldValue('vendorid'); const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) { if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId); const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
if (matchingVendors.length === 1) { if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount); setDiscount(matchingVendors[0].discount);
} }
} }
const jobId = form.getFieldValue('jobid'); const jobId = form.getFieldValue("jobid");
if (jobId) { if (jobId) {
loadLines({ variables: { id: jobId } }); loadLines({ variables: { id: jobId } });
if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) { if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({ loadOutstandingReturns({
variables: { variables: {
jobId: jobId, jobId: jobId,
vendorId: vendorId, vendorId: vendorId
}, }
}); });
} }
} }
@@ -118,24 +118,24 @@ export function BillFormComponent({
setDiscount, setDiscount,
vendorAutoCompleteOptions, vendorAutoCompleteOptions,
loadLines, loadLines,
bodyshop.inhousevendorid, bodyshop.inhousevendorid
]); ]);
return ( return (
<div> <div>
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form} />
<Form.Item style={{ display: 'none' }} name="isinhouse" valuePropName="checked"> <Form.Item style={{ display: "none" }} name="isinhouse" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
name="jobid" name="jobid"
label={t('bills.fields.ro_number')} label={t("bills.fields.ro_number")}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<JobSearchSelect <JobSearchSelect
@@ -143,20 +143,14 @@ export function BillFormComponent({
convertedOnly convertedOnly
notExported={false} notExported={false}
onBlur={() => { onBlur={() => {
if ( if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
form.getFieldValue('jobid') !== null && loadLines({ variables: { id: form.getFieldValue("jobid") } });
form.getFieldValue('jobid') !== undefined if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
) {
loadLines({ variables: { id: form.getFieldValue('jobid') } });
if (
form.getFieldValue('vendorid') !== null &&
form.getFieldValue('vendorid') !== undefined
) {
loadOutstandingReturns({ loadOutstandingReturns({
variables: { variables: {
jobId: form.getFieldValue('jobid'), jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue('vendorid'), vendorId: form.getFieldValue("vendorid")
}, }
}); });
} }
} }
@@ -164,22 +158,22 @@ export function BillFormComponent({
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('bills.fields.vendor')} label={t("bills.fields.vendor")}
name="vendorid" name="vendorid"
// style={{ display: billEdit ? "none" : null }} // style={{ display: billEdit ? "none" : null }}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) { if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) {
return Promise.reject(t('bills.validation.manualinhouse')); return Promise.reject(t("bills.validation.manualinhouse"));
} }
return Promise.resolve(); return Promise.resolve();
}, }
}), })
]} ]}
> >
<VendorSearchSelect <VendorSearchSelect
@@ -199,12 +193,8 @@ export function BillFormComponent({
type="warning" type="warning"
message={ message={
<Space> <Space>
{t('bills.labels.iouexists')} {t("bills.labels.iouexists")}
<Link <Link target="_blank" rel="noopener noreferrer" to={`/manage/jobs/${iou.id}?tab=repairdata`}>
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space> <Space>
{iou.ro_number} {iou.ro_number}
<Icon component={MdOpenInNew} /> <Icon component={MdOpenInNew} />
@@ -216,89 +206,85 @@ export function BillFormComponent({
))} ))}
<LayoutFormRow> <LayoutFormRow>
<Form.Item <Form.Item
label={t('bills.fields.invoice_number')} label={t("bills.fields.invoice_number")}
name="invoice_number" name="invoice_number"
validateTrigger="onBlur" validateTrigger="onBlur"
hasFeedback hasFeedback
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
async validator(rule, value) { async validator(rule, value) {
const vendorid = getFieldValue('vendorid'); const vendorid = getFieldValue("vendorid");
if (vendorid && value) { if (vendorid && value) {
const response = await client.query({ const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER, query: CHECK_BILL_INVOICE_NUMBER,
variables: { variables: {
invoice_number: value, invoice_number: value,
vendorid: vendorid, vendorid: vendorid
}, }
}); });
if (response.data.bills_aggregate.aggregate.count === 0) { if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve(); return Promise.resolve();
} else if ( } else if (
response.data.bills_aggregate.nodes.length === 1 && 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.resolve();
} }
return Promise.reject(t('bills.validation.unique_invoice_number')); return Promise.reject(t("bills.validation.unique_invoice_number"));
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}, }
}), })
]} ]}
> >
<Input disabled={disabled || disableInvNumber} /> <Input disabled={disabled || disableInvNumber} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('bills.fields.date')} label={t("bills.fields.date")}
name="date" name="date"
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) { if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if ( if (
dayjs(value) dayjs(value)
.startOf('day') .startOf("day")
.isSameOrAfter( .isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
) &&
dayjs(value) dayjs(value)
.startOf('day') .startOf("day")
.isSameOrBefore( .isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day"))
dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
)
) { ) {
return Promise.resolve(); return Promise.resolve();
} else { } else {
return Promise.reject(t('bills.validation.closingperiod')); return Promise.reject(t("bills.validation.closingperiod"));
} }
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
}, }
}), })
]} ]}
> >
<FormDatePicker disabled={disabled} /> <FormDatePicker disabled={disabled} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('bills.fields.is_credit_memo')} label={t("bills.fields.is_credit_memo")}
name="is_credit_memo" name="is_credit_memo"
valuePropName="checked" valuePropName="checked"
rules={[ rules={[
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { 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. //Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({ // loadOutstandingReturns({
// variables: { // variables: {
@@ -316,31 +302,31 @@ export function BillFormComponent({
job.status === bodyshop.md_ro_statuses.default_void) && job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value) (value === false || !value)
) { ) {
return Promise.reject(t('bills.labels.onlycmforinvoiced')); return Promise.reject(t("bills.labels.onlycmforinvoiced"));
} }
return Promise.resolve(); return Promise.resolve();
}, }
}), })
]} ]}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('bills.fields.total')} label={t("bills.fields.total")}
name="total" name="total"
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
{!billEdit && ( {!billEdit && (
<Form.Item label={t('bills.fields.allpartslocation')} name="location"> <Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: '10rem' }} disabled={disabled} allowClear> <Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
{loc} {loc}
@@ -353,40 +339,36 @@ export function BillFormComponent({
<LayoutFormRow> <LayoutFormRow>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<Form.Item span={3} label={t('bills.fields.federal_tax_rate')} name="federal_tax_rate"> <Form.Item span={3} label={t("bills.fields.federal_tax_rate")} name="federal_tax_rate">
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
), )
})} })}
<Form.Item span={3} label={t('bills.fields.state_tax_rate')} name="state_tax_rate"> <Form.Item span={3} label={t("bills.fields.state_tax_rate")} name="state_tax_rate">
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled} />
</Form.Item> </Form.Item>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<> <>
<Form.Item span={3} label={t('bills.fields.local_tax_rate')} name="local_tax_rate"> <Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item <Form.Item span={2} label={t("bills.labels.federal_tax_exempt")} name="federal_tax_exempt">
span={2}
label={t('bills.labels.federal_tax_exempt')}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} /> <Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item> </Form.Item>
) : null} ) : null}
</> </>
), )
})} })}
<Form.Item shouldUpdate span={13}> <Form.Item shouldUpdate span={13}>
{() => { {() => {
const values = form.getFieldsValue([ const values = form.getFieldsValue([
'billlines', "billlines",
'total', "total",
'federal_tax_rate', "federal_tax_rate",
'state_tax_rate', "state_tax_rate",
'local_tax_rate', "local_tax_rate"
]); ]);
let totals; let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0) if (!!values.total && !!values.billlines && values.billlines.length > 0)
@@ -394,56 +376,48 @@ export function BillFormComponent({
if (!!totals) if (!!totals)
return ( return (
<div align="right"> <div align="right">
<Space size='large' wrap> <Space size="large" wrap>
<Statistic <Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
title={t('bills.labels.subtotal')}
value={totals.subtotal.toFormat()}
precision={2}
/>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<Statistic <Statistic
title={t('bills.labels.federal_tax')} title={t("bills.labels.federal_tax")}
value={totals.federalTax.toFormat()} value={totals.federalTax.toFormat()}
precision={2} precision={2}
/> />
), )
})} })}
<Statistic <Statistic title={t("bills.labels.state_tax")} value={totals.stateTax.toFormat()} precision={2} />
title={t('bills.labels.state_tax')}
value={totals.stateTax.toFormat()}
precision={2}
/>
{InstanceRenderManager({ {InstanceRenderManager({
imex: ( imex: (
<Statistic <Statistic
title={t('bills.labels.local_tax')} title={t("bills.labels.local_tax")}
value={totals.localTax.toFormat()} value={totals.localTax.toFormat()}
precision={2} precision={2}
/> />
), )
})} })}
<Statistic <Statistic
title={t('bills.labels.entered_total')} title={t("bills.labels.entered_total")}
value={totals.enteredTotal.toFormat()} value={totals.enteredTotal.toFormat()}
precision={2} precision={2}
/> />
<Statistic <Statistic
title={t('bills.labels.bill_total')} title={t("bills.labels.bill_total")}
value={totals.invoiceTotal.toFormat()} value={totals.invoiceTotal.toFormat()}
precision={2} precision={2}
/> />
<Statistic <Statistic
title={t('bills.labels.discrepancy')} title={t("bills.labels.discrepancy")}
valueStyle={{ valueStyle={{
color: totals.discrepancy.getAmount() === 0 ? 'green' : 'red', color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
}} }}
value={totals.discrepancy.toFormat()} value={totals.discrepancy.toFormat()}
precision={2} precision={2}
/> />
</Space> </Space>
{form.getFieldValue('is_credit_memo') ? ( {form.getFieldValue("is_credit_memo") ? (
<AlertComponent type="warning" message={t('bills.labels.enteringcreditmemo')} /> <AlertComponent type="warning" message={t("bills.labels.enteringcreditmemo")} />
) : null} ) : null}
</div> </div>
); );
@@ -451,9 +425,9 @@ export function BillFormComponent({
}} }}
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Divider orientation="left">{t('bills.labels.bill_lines')}</Divider> <Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === 'on' ? ( {Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended <BillFormLinesExtended
lineData={lineData} lineData={lineData}
discount={discount} discount={discount}
@@ -471,13 +445,13 @@ export function BillFormComponent({
billEdit={billEdit} billEdit={billEdit}
/> />
)} )}
<Divider orientation="left" style={{ display: billEdit ? 'none' : null }}> <Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
{t('documents.labels.upload')} {t("documents.labels.upload")}
</Divider> </Divider>
<Form.Item <Form.Item
name="upload" name="upload"
label="Upload" label="Upload"
style={{ display: billEdit ? 'none' : null }} style={{ display: billEdit ? "none" : null }}
valuePropName="fileList" valuePropName="fileList"
getValueFromEvent={(e) => { getValueFromEvent={(e) => {
if (Array.isArray(e)) { if (Array.isArray(e)) {

View File

@@ -13,35 +13,27 @@ import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.com
import BillFormComponent from "./bill-form.component"; import BillFormComponent from "./bill-form.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export function BillFormContainer({ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
bodyshop, const {
form, treatments: { Simple_Inventory }
billEdit, } = useSplitTreatments({
disabled,
disableInvNumber,
}) {
const {treatments: {Simple_Inventory}} = useSplitTreatments({
attributes: {}, attributes: {},
names: ["Simple_Inventory"], names: ["Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid, splitKey: bodyshop && bodyshop.imexshopid
}); });
const {data: VendorAutoCompleteData} = useQuery( const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
SEARCH_VENDOR_AUTOCOMPLETE, fetchPolicy: "network-only",
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"} nextFetchPolicy: "network-only"
); });
const [loadLines, {data: lineData}] = useLazyQuery( const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
GET_JOB_LINES_TO_ENTER_BILL
);
const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] = const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
useLazyQuery(QUERY_UNRECEIVED_LINES); const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return ( return (
<> <>
@@ -49,9 +41,7 @@ export function BillFormContainer({
disabled={disabled} disabled={disabled}
form={form} form={form}
billEdit={billEdit} billEdit={billEdit}
vendorAutoCompleteOptions={ vendorAutoCompleteOptions={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines} loadLines={loadLines}
lineData={lineData ? lineData.joblines : []} lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null} job={lineData ? lineData.jobs_by_pk : null}
@@ -61,13 +51,7 @@ export function BillFormContainer({
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/> />
{!billEdit && ( {!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
<BillCmdReturnsTableComponent
form={form}
returnLoading={returnLoading}
returnData={returnData}
/>
)}
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<BillInventoryTable <BillInventoryTable
form={form} form={form}

View File

@@ -1,17 +1,6 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
Button,
Checkbox,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip,
} from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -25,7 +14,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -39,17 +28,17 @@ export function BillEnterModalLinesComponent({
form, form,
responsibilityCenters, responsibilityCenters,
billEdit, billEdit,
billid, billid
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { const {
treatments: { Simple_Inventory, Enhanced_Payroll }, treatments: { Simple_Inventory, Enhanced_Payroll }
} = useSplitTreatments({ } = useSplitTreatments({
attributes: {}, attributes: {},
names: ["Simple_Inventory", "Enhanced_Payroll"], names: ["Simple_Inventory", "Enhanced_Payroll"],
splitKey: bodyshop && bodyshop.imexshopid, splitKey: bodyshop && bodyshop.imexshopid
}); });
const columns = (remove) => { const columns = (remove) => {
@@ -66,19 +55,14 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.jobline"), label: t("billlines.fields.jobline"),
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
], ]
}; };
}, },
wrapper: (props) => ( wrapper: (props) => (
<Form.Item <Form.Item noStyle shouldUpdate={(prev, cur) => prev.is_credit_memo !== cur.is_credit_memo}>
noStyle
shouldUpdate={(prev, cur) =>
prev.is_credit_memo !== cur.is_credit_memo
}
>
{() => { {() => {
return props.children; return props.children;
}} }}
@@ -89,14 +73,10 @@ export function BillEnterModalLinesComponent({
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ width: "100%", minWidth: "10rem" }} style={{ width: "100%", minWidth: "10rem" }}
allowRemoved={ allowRemoved={form.getFieldValue("is_credit_memo") || false}
form.getFieldValue("is_credit_memo") || false
}
onSelect={(value, opt) => { onSelect={(value, opt) => {
setFieldsValue({ setFieldsValue({
billlines: getFieldsValue([ billlines: getFieldsValue(["billlines"]).billlines.map((item, idx) => {
"billlines",
]).billlines.map((item, idx) => {
if (idx === index) { if (idx === index) {
return { return {
...item, ...item,
@@ -105,26 +85,21 @@ export function BillEnterModalLinesComponent({
actual_price: opt.cost, actual_price: opt.cost,
original_actual_price: opt.cost, original_actual_price: opt.cost,
cost_center: opt.part_type cost_center: opt.part_type
? bodyshop.pbs_serialnumber || ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
bodyshop.cdk_dealerid
? opt.part_type !== "PAE" ? opt.part_type !== "PAE"
? opt.part_type ? opt.part_type
: null : null
: responsibilityCenters.defaults && : responsibilityCenters.defaults &&
(responsibilityCenters (responsibilityCenters.defaults.costs[opt.part_type] || null)
.defaults.costs[ : null
opt.part_type
] ||
null)
: null,
}; };
} }
return item; return item;
}), })
}); });
}} }}
/> />
), )
}, },
{ {
title: t("billlines.fields.line_desc"), title: t("billlines.fields.line_desc"),
@@ -138,13 +113,13 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"), label: t("billlines.fields.line_desc"),
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
], ]
}; };
}, },
formInput: (record, index) => <Input disabled={disabled} />, formInput: (record, index) => <Input disabled={disabled} />
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
@@ -158,38 +133,25 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.quantity"), label: t("billlines.fields.quantity"),
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
validator(rule, value) { validator(rule, value) {
if ( if (value && getFieldValue("billlines")[field.fieldKey]?.inventories?.length > value) {
value &&
getFieldValue("billlines")[
field.fieldKey
]?.inventories?.length > value
) {
return Promise.reject( return Promise.reject(
t( t("bills.validation.inventoryquantity", {
"bills.validation.inventoryquantity", number: getFieldValue("billlines")[field.fieldKey]?.inventories?.length
{ })
number: getFieldValue(
"billlines"
)[field.fieldKey]
?.inventories?.length,
}
)
); );
} }
return Promise.resolve(); return Promise.resolve();
}, }
}), })
], ]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => <InputNumber precision={0} min={1} disabled={disabled} />
<InputNumber precision={0} min={1} disabled={disabled} />
),
}, },
{ {
title: t("billlines.fields.actual_price"), title: t("billlines.fields.actual_price"),
@@ -203,10 +165,10 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.actual_price"), label: t("billlines.fields.actual_price"),
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
], ]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
@@ -215,26 +177,17 @@ export function BillEnterModalLinesComponent({
disabled={disabled} disabled={disabled}
onBlur={(e) => { onBlur={(e) => {
setFieldsValue({ setFieldsValue({
billlines: getFieldsValue( billlines: getFieldsValue("billlines").billlines.map((item, idx) => {
"billlines"
).billlines.map((item, idx) => {
if (idx === index) { if (idx === index) {
return { return {
...item, ...item,
actual_cost: !!item.actual_cost actual_cost: !!item.actual_cost
? item.actual_cost ? item.actual_cost
: Math.round( : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
(parseFloat(
e.target.value
) *
(1 - discount) +
Number.EPSILON) *
100
) / 100,
}; };
} }
return item; return item;
}), })
}); });
}} }}
/> />
@@ -242,50 +195,24 @@ export function BillEnterModalLinesComponent({
additional: (record, index) => additional: (record, index) =>
InstanceRenderManager({ InstanceRenderManager({
rome: ( rome: (
<Form.Item <Form.Item dependencies={["billlines", record.name, "actual_price"]} noStyle>
dependencies={[
"billlines",
record.name,
"actual_price",
]}
noStyle
>
{() => { {() => {
const billLine = getFieldValue([ const billLine = getFieldValue(["billlines", record.name]);
"billlines", const jobLine = lineData.find((line) => line.id === billLine?.joblineid);
record.name,
]);
const jobLine = lineData.find(
(line) =>
line.id === billLine?.joblineid
);
if ( if (!billEdit && billLine && jobLine && billLine?.actual_price !== jobLine?.act_price) {
!billEdit &&
billLine &&
jobLine &&
billLine?.actual_price !==
jobLine?.act_price
) {
return ( return (
<Space size="small"> <Space size="small">
<Form.Item <Form.Item
noStyle noStyle
label={t( label={t("joblines.fields.create_ppc")}
"joblines.fields.create_ppc"
)}
key={`${index}ppc`} key={`${index}ppc`}
valuePropName="checked" valuePropName="checked"
name={[ name={[record.name, "create_ppc"]}
record.name,
"create_ppc",
]}
> >
<Checkbox /> <Checkbox />
</Form.Item> </Form.Item>
{t( {t("joblines.fields.create_ppc")}
"joblines.fields.create_ppc"
)}
</Space> </Space>
); );
} else { } else {
@@ -293,9 +220,9 @@ export function BillEnterModalLinesComponent({
} }
}} }}
</Form.Item> </Form.Item>
), )
//Do not need to set for promanager as it will default to Rome. //Do not need to set for promanager as it will default to Rome.
}), })
}, },
{ {
title: t("billlines.fields.actual_cost"), title: t("billlines.fields.actual_cost"),
@@ -310,10 +237,10 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.actual_cost"), label: t("billlines.fields.actual_cost"),
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
], ]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
@@ -324,33 +251,20 @@ export function BillEnterModalLinesComponent({
addonAfter={ addonAfter={
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
{() => { {() => {
const line = getFieldsValue(["billlines"]) const line = getFieldsValue(["billlines"]).billlines[index];
.billlines[index];
if (!!!line) return null; if (!!!line) return null;
let lineDiscount = let lineDiscount = 1 - line.actual_cost / line.actual_price;
1 -
line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0; if (isNaN(lineDiscount)) lineDiscount = 0;
return ( return (
<Tooltip <Tooltip title={`${(lineDiscount * 100).toFixed(2) || 0}%`}>
title={`${
(lineDiscount * 100).toFixed(
2
) || 0
}%`}
>
<DollarCircleFilled <DollarCircleFilled
style={{ style={{
color: color:
Math.abs( Math.abs(lineDiscount - discount) > 0.005
lineDiscount - ? lineDiscount > discount
discount
) > 0.005
? lineDiscount >
discount
? "orange" ? "orange"
: "red" : "red"
: "green", : "green"
}} }}
/> />
</Tooltip> </Tooltip>
@@ -359,7 +273,7 @@ export function BillEnterModalLinesComponent({
</Form.Item> </Form.Item>
} }
/> />
), )
// additional: (record, index) => ( // additional: (record, index) => (
// <Form.Item shouldUpdate> // <Form.Item shouldUpdate>
// {() => { // {() => {
@@ -396,27 +310,19 @@ export function BillEnterModalLinesComponent({
valuePropName: "value", valuePropName: "value",
rules: [ rules: [
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
], ]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
<Select <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
showSearch
style={{ minWidth: "3rem" }}
disabled={disabled}
>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false) ? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ( : responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select> </Select>
), )
}, },
...(billEdit ...(billEdit
? [] ? []
@@ -429,21 +335,19 @@ export function BillEnterModalLinesComponent({
formItemProps: (field) => { formItemProps: (field) => {
return { return {
key: `${field.index}location`, key: `${field.index}location`,
name: [field.name, "location"], name: [field.name, "location"]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
<Select disabled={disabled}> <Select disabled={disabled}>
{bodyshop.md_parts_locations.map( {bodyshop.md_parts_locations.map((loc, idx) => (
(loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
{loc} {loc}
</Select.Option> </Select.Option>
) ))}
)}
</Select> </Select>
), )
}, }
]), ]),
{ {
title: t("billlines.labels.deductedfromlbr"), title: t("billlines.labels.deductedfromlbr"),
@@ -453,231 +357,100 @@ export function BillEnterModalLinesComponent({
return { return {
valuePropName: "checked", valuePropName: "checked",
key: `${field.index}deductedfromlbr`, key: `${field.index}deductedfromlbr`,
name: [field.name, "deductedfromlbr"], name: [field.name, "deductedfromlbr"]
}; };
}, },
formInput: (record, index) => <Switch disabled={disabled} />, formInput: (record, index) => <Switch disabled={disabled} />,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item <Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
shouldUpdate
noStyle
style={{ display: "inline-block" }}
>
{() => { {() => {
const price = getFieldValue([ const price = getFieldValue(["billlines", record.name, "actual_price"]);
"billlines",
record.name,
"actual_price",
]);
const adjustmentRate = getFieldValue([ const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]);
"billlines",
record.name,
"lbr_adjustment",
"rate",
]);
const billline = getFieldValue([ const billline = getFieldValue(["billlines", record.name]);
"billlines",
record.name,
]);
const jobline = lineData.find( const jobline = lineData.find((line) => line.id === billline?.joblineid);
(line) => line.id === billline?.joblineid
);
const employeeTeamName = const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team);
bodyshop.employee_teams.find(
(team) => team.id === jobline?.assigned_team
);
if ( if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
getFieldValue([
"billlines",
record.name,
"deductedfromlbr",
])
)
return ( return (
<div> <div>
{Enhanced_Payroll.treatment === "on" ? ( {Enhanced_Payroll.treatment === "on" ? (
<Space> <Space>
{t( {t("joblines.fields.assigned_team", {
"joblines.fields.assigned_team", name: employeeTeamName?.name
{ })}
name: employeeTeamName?.name, {`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`}
}
)}
{`${
jobline.mod_lb_hrs
} units/${t(
`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`
)}`}
</Space> </Space>
) : null} ) : null}
<Form.Item <Form.Item
label={t( label={t("joblines.fields.mod_lbr_ty")}
"joblines.fields.mod_lbr_ty"
)}
key={`${index}modlbrty`} key={`${index}modlbrty`}
initialValue={ initialValue={jobline ? jobline.mod_lbr_ty : null}
jobline
? jobline.mod_lbr_ty
: null
}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]}
name={[
record.name,
"lbr_adjustment",
"mod_lbr_ty",
]} ]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select allowClear> <Select allowClear>
<Select.Option value="LAA"> <Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
{t( <Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
"joblines.fields.lbr_types.LAA" <Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
)} <Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
</Select.Option> <Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAB"> <Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
{t( <Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
"joblines.fields.lbr_types.LAB" <Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
)} <Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
</Select.Option> <Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LAD"> <Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
{t( <Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
"joblines.fields.lbr_types.LAD" <Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
)} <Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select.Option>
<Select.Option value="LAE">
{t(
"joblines.fields.lbr_types.LAE"
)}
</Select.Option>
<Select.Option value="LAF">
{t(
"joblines.fields.lbr_types.LAF"
)}
</Select.Option>
<Select.Option value="LAG">
{t(
"joblines.fields.lbr_types.LAG"
)}
</Select.Option>
<Select.Option value="LAM">
{t(
"joblines.fields.lbr_types.LAM"
)}
</Select.Option>
<Select.Option value="LAR">
{t(
"joblines.fields.lbr_types.LAR"
)}
</Select.Option>
<Select.Option value="LAS">
{t(
"joblines.fields.lbr_types.LAS"
)}
</Select.Option>
<Select.Option value="LAU">
{t(
"joblines.fields.lbr_types.LAU"
)}
</Select.Option>
<Select.Option value="LA1">
{t(
"joblines.fields.lbr_types.LA1"
)}
</Select.Option>
<Select.Option value="LA2">
{t(
"joblines.fields.lbr_types.LA2"
)}
</Select.Option>
<Select.Option value="LA3">
{t(
"joblines.fields.lbr_types.LA3"
)}
</Select.Option>
<Select.Option value="LA4">
{t(
"joblines.fields.lbr_types.LA4"
)}
</Select.Option>
</Select> </Select>
</Form.Item> </Form.Item>
{Enhanced_Payroll.treatment === "on" ? ( {Enhanced_Payroll.treatment === "on" ? (
<Form.Item <Form.Item
label={t( label={t("billlines.labels.mod_lbr_adjustment")}
"billlines.labels.mod_lbr_adjustment" name={[record.name, "lbr_adjustment", "mod_lb_hrs"]}
)}
name={[
record.name,
"lbr_adjustment",
"mod_lb_hrs",
]}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<InputNumber <InputNumber precision={5} min={0.01} max={jobline ? jobline.mod_lb_hrs : 0} />
precision={5}
min={0.01}
max={
jobline
? jobline.mod_lb_hrs
: 0
}
/>
</Form.Item> </Form.Item>
) : ( ) : (
<Form.Item <Form.Item
label={t( label={t("jobs.labels.adjustmentrate")}
"jobs.labels.adjustmentrate" name={[record.name, "lbr_adjustment", "rate"]}
)} initialValue={bodyshop.default_adjustment_rate}
name={[
record.name,
"lbr_adjustment",
"rate",
]}
initialValue={
bodyshop.default_adjustment_rate
}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<InputNumber <InputNumber precision={2} min={0.01} />
precision={2}
min={0.01}
/>
</Form.Item> </Form.Item>
)} )}
<Space> <Space>{price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}</Space>
{price &&
adjustmentRate &&
`${(
price / adjustmentRate
).toFixed(1)} hrs`}
</Space>
</div> </div>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>
), )
}, },
...InstanceRenderManager({ ...InstanceRenderManager({
@@ -692,20 +465,18 @@ export function BillEnterModalLinesComponent({
formItemProps: (field) => { formItemProps: (field) => {
return { return {
key: `${field.index}fedtax`, key: `${field.index}fedtax`,
valuePropName: 'checked', valuePropName: "checked",
initialValue: InstanceRenderManager({ initialValue: InstanceRenderManager({
imex: true, imex: true,
rome: false, rome: false,
promanager: false, promanager: false
}), }),
name: [field.name, 'applicable_taxes', 'federal'], name: [field.name, "applicable_taxes", "federal"]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => <Switch disabled={disabled} />
<Switch disabled={disabled} /> }
), ]
},
],
}), }),
{ {
@@ -717,10 +488,10 @@ export function BillEnterModalLinesComponent({
return { return {
key: `${field.index}statetax`, key: `${field.index}statetax`,
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"], name: [field.name, "applicable_taxes", "state"]
}; };
}, },
formInput: (record, index) => <Switch disabled={disabled} />, formInput: (record, index) => <Switch disabled={disabled} />
}, },
...InstanceRenderManager({ ...InstanceRenderManager({
@@ -736,14 +507,12 @@ export function BillEnterModalLinesComponent({
return { return {
key: `${field.index}localtax`, key: `${field.index}localtax`,
valuePropName: "checked", valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"], name: [field.name, "applicable_taxes", "local"]
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => <Switch disabled={disabled} />
<Switch disabled={disabled} /> }
), ]
},
],
}), }),
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -754,36 +523,23 @@ export function BillEnterModalLinesComponent({
{() => ( {() => (
<Space wrap> <Space wrap>
<Button <Button
disabled={ disabled={disabled || getFieldValue("billlines")[record.fieldKey]?.inventories?.length > 0}
disabled ||
getFieldValue("billlines")[
record.fieldKey
]?.inventories?.length > 0
}
onClick={() => remove(record.name)} onClick={() => remove(record.name)}
> >
<DeleteFilled /> <DeleteFilled />
</Button> </Button>
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<BilllineAddInventory <BilllineAddInventory
disabled={ disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")}
!billEdit || billline={getFieldValue("billlines")[record.fieldKey]}
form.isFieldsTouched() ||
form.getFieldValue("is_credit_memo")
}
billline={
getFieldValue("billlines")[
record.fieldKey
]
}
jobid={getFieldValue("jobid")} jobid={getFieldValue("jobid")}
/> />
)} )}
</Space> </Space>
)} )}
</Form.Item> </Form.Item>
), )
}, }
]; ];
}; };
@@ -798,8 +554,8 @@ export function BillEnterModalLinesComponent({
formInput: col.formInput, formInput: col.formInput,
additional: col.additional, additional: col.additional,
dataIndex: col.dataIndex, dataIndex: col.dataIndex,
title: col.title, title: col.title
}), })
}; };
}); });
@@ -810,12 +566,10 @@ export function BillEnterModalLinesComponent({
{ {
validator: async (_, billlines) => { validator: async (_, billlines) => {
if (!billlines || billlines.length < 1) { if (!billlines || billlines.length < 1) {
return Promise.reject( return Promise.reject(new Error(t("billlines.validation.atleastone")));
new Error(t("billlines.validation.atleastone")) }
); }
} }
},
},
]} ]}
> >
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
@@ -824,8 +578,8 @@ export function BillEnterModalLinesComponent({
<Table <Table
components={{ components={{
body: { body: {
cell: EditableCell, cell: EditableCell
}, }
}} }}
size="small" size="small"
bordered bordered
@@ -853,10 +607,7 @@ export function BillEnterModalLinesComponent({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent);
mapStateToProps,
mapDispatchToProps
)(BillEnterModalLinesComponent);
const EditableCell = ({ const EditableCell = ({
dataIndex, dataIndex,
@@ -875,13 +626,8 @@ const EditableCell = ({
return ( return (
<td {...restProps}> <td {...restProps}>
<div size="small"> <div size="small">
<Form.Item <Form.Item name={dataIndex} labelCol={{ span: 0 }} {...(formItemProps && formItemProps(record))}>
name={dataIndex} {(formInput && formInput(record, record.name)) || children}
labelCol={{ span: 0 }}
{...(formItemProps && formItemProps(record))}
>
{(formInput && formInput(record, record.name)) ||
children}
</Form.Item> </Form.Item>
{additional && additional(record, record.name)} {additional && additional(record, record.name)}
</div> </div>
@@ -891,24 +637,15 @@ const EditableCell = ({
return ( return (
<wrapper> <wrapper>
<td {...restProps}> <td {...restProps}>
<Form.Item <Form.Item labelCol={{ span: 0 }} name={dataIndex} {...(formItemProps && formItemProps(record))}>
labelCol={{ span: 0 }} {(formInput && formInput(record, record.name)) || children}
name={dataIndex}
{...(formItemProps && formItemProps(record))}
>
{(formInput && formInput(record, record.name)) ||
children}
</Form.Item> </Form.Item>
</td> </td>
</wrapper> </wrapper>
); );
return ( return (
<td {...restProps}> <td {...restProps}>
<Form.Item <Form.Item labelCol={{ span: 0 }} name={dataIndex} {...(formItemProps && formItemProps(record))}>
labelCol={{ span: 0 }}
name={dataIndex}
{...(formItemProps && formItemProps(record))}
>
{(formInput && formInput(record, record.name)) || children} {(formInput && formInput(record, record.name)) || children}
</Form.Item> </Form.Item>
</td> </td>

View File

@@ -1,8 +1,7 @@
import Dinero from "dinero.js"; import Dinero from "dinero.js";
export const CalculateBillTotal = (invoice) => { export const CalculateBillTotal = (invoice) => {
const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} = const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
invoice;
//TODO Determine why this recalculates so many times. //TODO Determine why this recalculates so many times.
let subtotal = Dinero({ amount: 0 }); let subtotal = Dinero({ amount: 0 });
@@ -15,19 +14,15 @@ export const CalculateBillTotal = (invoice) => {
billlines.forEach((i) => { billlines.forEach((i) => {
if (!!i) { if (!!i) {
const itemTotal = Dinero({ const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100), amount: Math.round((i.actual_cost || 0) * 100)
}).multiply(i.quantity || 1); }).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal); subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes?.federal) { if (i.applicable_taxes?.federal) {
federalTax = federalTax.add( federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0));
itemTotal.percentage(federal_tax_rate || 0)
);
} }
if (i.applicable_taxes?.state) if (i.applicable_taxes?.state) stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0)); if (i.applicable_taxes?.local) localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
if (i.applicable_taxes?.local)
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
} }
}); });
@@ -42,6 +37,6 @@ export const CalculateBillTotal = (invoice) => {
localTax, localTax,
enteredTotal, enteredTotal,
invoiceTotal, invoiceTotal,
discrepancy, discrepancy
}; };
}; };

View File

@@ -11,21 +11,14 @@ import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable); export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({ export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
billEnterModal,
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -33,23 +26,18 @@ export function BillInventoryTable({
form.setFieldsValue({ form.setFieldsValue({
inventory: billEnterModal.context.consumeinventoryid inventory: billEnterModal.context.consumeinventoryid
? inventoryData.inventory.map((i) => { ? inventoryData.inventory.map((i) => {
if (i.id === billEnterModal.context.consumeinventoryid) if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
i.consumefrominventory = true;
return i; return i;
}) })
: inventoryData.inventory, : inventoryData.inventory
}); });
} }
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]); }, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
return ( return (
<Form.Item <Form.Item shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid} noStyle>
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => { {() => {
const is_inhouse = const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) { if (!is_inhouse || billEdit) {
return null; return null;
@@ -62,9 +50,7 @@ export function BillInventoryTable({
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
<> <>
<Typography.Title level={4}> <Typography.Title level={4}>{t("inventory.labels.inventory")}</Typography.Title>
{t("inventory.labels.inventory")}
</Typography.Title>
<table className="bill-inventory-table"> <table className="bill-inventory-table">
<thead> <thead>
<tr> <tr>
@@ -95,13 +81,7 @@ export function BillInventoryTable({
span={2} span={2}
//label={t("joblines.fields.mod_lb_hrs")} //label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`} key={`${index}part_type`}
name={[ name={[field.name, "billline", "bill", "vendor", "name"]}
field.name,
"billline",
"bill",
"vendor",
"name",
]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent />
</Form.Item> </Form.Item>

View File

@@ -1,14 +1,11 @@
import { Select } from "antd"; import { Select } from "antd";
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import InstanceRenderMgr from '../../utils/instanceRenderMgr'; import InstanceRenderMgr from "../../utils/instanceRenderMgr";
//To be used as a form element only. //To be used as a form element only.
const { Option } = Select; const { Option } = Select;
const BillLineSearchSelect = ( const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
{options, disabled, allowRemoved, ...restProps},
ref
) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -21,20 +18,10 @@ const BillLineSearchSelect = (
// optionFilterProp="line_desc" // optionFilterProp="line_desc"
filterOption={(inputValue, option) => { filterOption={(inputValue, option) => {
return ( return (
(option.line_desc && (option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) ||
option.line_desc (option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
.toLowerCase() (option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
.includes(inputValue.toLowerCase())) || (option.act_price && option.act_price.toString().startsWith(inputValue.toString()))
(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."} notFoundContent={"Removed."}
@@ -57,7 +44,7 @@ const BillLineSearchSelect = (
alt_partno={item.alt_partno} alt_partno={item.alt_partno}
act_price={item.act_price} act_price={item.act_price}
style={{ style={{
...(item.removed ? {textDecoration: "line-through"} : {}), ...(item.removed ? { textDecoration: "line-through" } : {})
}} }}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : "" item.oem_partno ? ` - ${item.oem_partno}` : ""
@@ -68,26 +55,14 @@ const BillLineSearchSelect = (
item.oem_partno ? ` - ${item.oem_partno}` : "" item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()} }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span> </span>
{ {InstanceRenderMgr({
InstanceRenderMgr
(
{
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && ( rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
<span style={{float: "right", paddingleft: "1rem"}}> <span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
{`${item.mod_lb_hrs} units`}
</span>
) )
})}
}
)
}
<span style={{ float: "right", paddingleft: "1rem" }}> <span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price {item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
</span> </span>
</Option> </Option>
)) ))

View File

@@ -5,30 +5,22 @@ import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
mapStateToProps,
mapDispatchToProps
)(BillMarkExportedButton);
export function BillMarkExportedButton({ export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
currentUser,
bodyshop,
authLevel,
bill,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
@@ -48,7 +40,7 @@ export function BillMarkExportedButton({
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: {billId: bill.id}, variables: { billId: bill.id }
}); });
await insertExportLog({ await insertExportLog({
@@ -59,21 +51,21 @@ export function BillMarkExportedButton({
billid: bill.id, billid: bill.id,
successful: true, successful: true,
message: JSON.stringify([t("general.labels.markedexported")]), message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email, useremail: currentUser.email
}, }
], ]
}, }
}); });
if (!result.errors) { if (!result.errors) {
notification["success"]({ notification["success"]({
message: t("bills.successes.markexported"), message: t("bills.successes.markexported")
}); });
} else { } else {
notification["error"]({ notification["error"]({
message: t("bills.errors.saving", { message: t("bills.errors.saving", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors)
}), })
}); });
} }
setLoading(false); setLoading(false);
@@ -83,7 +75,7 @@ export function BillMarkExportedButton({
const hasAccess = HasRbacAccess({ const hasAccess = HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,
action: "bills:reexport", action: "bills:reexport"
}); });
if (hasAccess) if (hasAccess)

View File

@@ -16,8 +16,8 @@ export default function BillPrintButton({billid}) {
{ {
name: Templates.parts_invoice_label_single.key, name: Templates.parts_invoice_label_single.key,
variables: { variables: {
id: billid, id: billid
}, }
}, },
{}, {},
"p" "p"

View File

@@ -5,21 +5,18 @@ import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
mapStateToProps,
mapDispatchToProps
)(BillMarkForReexportButton);
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) { export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -40,18 +37,18 @@ export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: {billId: bill.id}, variables: { billId: bill.id }
}); });
if (!result.errors) { if (!result.errors) {
notification["success"]({ notification["success"]({
message: t("bills.successes.reexport"), message: t("bills.successes.reexport")
}); });
} else { } else {
notification["error"]({ notification["error"]({
message: t("bills.errors.saving", { message: t("bills.errors.saving", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors)
}), })
}); });
} }
setLoading(false); setLoading(false);
@@ -61,16 +58,12 @@ export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
const hasAccess = HasRbacAccess({ const hasAccess = HasRbacAccess({
bodyshop, bodyshop,
authLevel, authLevel,
action: "bills:reexport", action: "bills:reexport"
}); });
if (hasAccess) if (hasAccess)
return ( return (
<Button <Button loading={loading} disabled={!bill.exported} onClick={handleUpdate}>
loading={loading}
disabled={!bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markforreexport")} {t("bills.labels.markforreexport")}
</Button> </Button>
); );

View File

@@ -7,30 +7,21 @@ import React, {useState} from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries"; import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string"; import queryString from "query-string";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({ export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search); const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT); const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
@@ -62,18 +53,17 @@ export function BilllineAddInventory({
applicable_taxes: { applicable_taxes: {
local: billline.applicable_taxes.local, local: billline.applicable_taxes.local,
state: billline.applicable_taxes.state, state: billline.applicable_taxes.state,
federal: billline.applicable_taxes.federal, federal: billline.applicable_taxes.federal
}, }
}, }
], ]
}; };
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100; cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
const insertResult = await insertInventoryLine({ const insertResult = await insertInventoryLine({
variables: { variables: {
joblineId: 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.
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. //Unfortunately, we can't send null as the GQL syntax validation fails.
joblineStatus: bodyshop.md_order_statuses.default_returned, joblineStatus: bodyshop.md_order_statuses.default_returned,
inv: { inv: {
@@ -82,7 +72,7 @@ export function BilllineAddInventory({
actual_price: billline.actual_price, actual_price: billline.actual_price,
actual_cost: billline.actual_cost, actual_cost: billline.actual_cost,
quantity: billline.quantity, quantity: billline.quantity,
line_desc: billline.line_desc, line_desc: billline.line_desc
}, },
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert. cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
pol: { pol: {
@@ -97,35 +87,34 @@ export function BilllineAddInventory({
act_price: billline.actual_price, act_price: billline.actual_price,
cost: billline.actual_cost, cost: billline.actual_cost,
quantity: billline.quantity, quantity: billline.quantity,
job_line_id: job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
billline.joblineid === "noline" ? null : billline.joblineid,
part_type: billline.jobline && billline.jobline.part_type, part_type: billline.jobline && billline.jobline.part_type,
cm_received: true, cm_received: true
}, }
], ]
}, },
order_date: "2022-06-01", order_date: "2022-06-01",
orderedby: currentUser.email, orderedby: currentUser.email,
jobid: jobid, jobid: jobid,
user_email: currentUser.email, user_email: currentUser.email,
return: true, return: true,
status: "Ordered", status: "Ordered"
}
}, },
}, refetchQueries: ["QUERY_BILL_BY_PK"]
refetchQueries: ["QUERY_BILL_BY_PK"],
}); });
if (!insertResult.errors) { if (!insertResult.errors) {
notification.open({ notification.open({
type: "success", type: "success",
message: t("inventory.successes.inserted"), message: t("inventory.successes.inserted")
}); });
} else { } else {
notification.open({ notification.open({
type: "error", type: "error",
message: t("inventory.errors.inserting", { message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors), error: JSON.stringify(insertResult.errors)
}), })
}); });
} }
@@ -136,15 +125,11 @@ export function BilllineAddInventory({
<Tooltip title={t("inventory.actions.addtoinventory")}> <Tooltip title={t("inventory.actions.addtoinventory")}>
<Button <Button
loading={loading} loading={loading}
disabled={ disabled={disabled || billline?.inventories?.length >= billline.quantity}
disabled || billline?.inventories?.length >= billline.quantity
}
onClick={addToInventory} onClick={addToInventory}
> >
<FileAddFilled /> <FileAddFilled />
{billline?.inventories?.length > 0 && ( {billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
<div>({billline?.inventories?.length} in inv)</div>
)}
</Button> </Button>
</Tooltip> </Tooltip>
); );

View File

@@ -17,16 +17,13 @@ import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
dispatch(setModalContext({context: context, modal: "partsOrder"})), setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
setBillEnterContext: (context) => setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
dispatch(setModalContext({context: context, modal: "billEnter"})),
setReconciliationContext: (context) =>
dispatch(setModalContext({context: context, modal: "reconciliation"})),
}); });
export function BillsListTableComponent({ export function BillsListTableComponent({
@@ -37,12 +34,12 @@ export function BillsListTableComponent({
handleOnRowClick, handleOnRowClick,
setPartsOrderContext, setPartsOrderContext,
setBillEnterContext, setBillEnterContext,
setReconciliationContext, setReconciliationContext
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {}
}); });
// const search = queryString.parse(useLocation().search); // const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid; // const selectedBill = search.billid;
@@ -61,18 +58,14 @@ export function BillsListTableComponent({
<BillDeleteButton bill={record} jobid={job.id} /> <BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
/> />
{record.isinhouse && ( {record.isinhouse && (
<PrintWrapperComponent <PrintWrapperComponent
templateObject={{ templateObject={{
name: Templates.inhouse_invoice.key, name: Templates.inhouse_invoice.key,
variables: {id: record.id}, variables: { id: record.id }
}} }}
messageObject={{ subject: Templates.inhouse_invoice.subject }} messageObject={{ subject: Templates.inhouse_invoice.subject }}
/> />
@@ -85,64 +78,54 @@ export function BillsListTableComponent({
dataIndex: "vendorname", dataIndex: "vendorname",
key: "vendorname", key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder: sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, render: (text, record) => <span>{record.vendor.name}</span>
render: (text, record) => <span>{record.vendor.name}</span>,
}, },
{ {
title: t("bills.fields.invoice_number"), title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number", dataIndex: "invoice_number",
key: "invoice_number", key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
}, },
{ {
title: t("bills.fields.date"), title: t("bills.fields.date"),
dataIndex: "date", dataIndex: "date",
key: "date", key: "date",
sorter: (a, b) => dateSort(a.date, b.date), sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
state.sortedInfo.columnKey === "date" && state.sortedInfo.order, render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
}, },
{ {
title: t("bills.fields.total"), title: t("bills.fields.total"),
dataIndex: "total", dataIndex: "total",
key: "total", key: "total",
sorter: (a, b) => a.total - b.total, sorter: (a, b) => a.total - b.total,
sortOrder: sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
state.sortedInfo.columnKey === "total" && state.sortedInfo.order, render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
render: (text, record) => (
<CurrencyFormatter>{record.total}</CurrencyFormatter>
),
}, },
{ {
title: t("bills.fields.is_credit_memo"), title: t("bills.fields.is_credit_memo"),
dataIndex: "is_credit_memo", dataIndex: "is_credit_memo",
key: "is_credit_memo", key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo, sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
state.sortedInfo.columnKey === "is_credit_memo" && render: (text, record) => <Checkbox checked={record.is_credit_memo} />
state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo}/>,
}, },
{ {
title: t("bills.fields.exported"), title: t("bills.fields.exported"),
dataIndex: "exported", dataIndex: "exported",
key: "exported", key: "exported",
sorter: (a, b) => a.exported - b.exported, sorter: (a, b) => a.exported - b.exported,
sortOrder: sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order, render: (text, record) => <Checkbox checked={record.exported} />
render: (text, record) => <Checkbox checked={record.exported}/>,
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => recordActions(record, true), render: (text, record) => recordActions(record, true)
}, }
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -154,16 +137,9 @@ export function BillsListTableComponent({
? bills ? bills
: bills.filter( : bills.filter(
(b) => (b) =>
(b.invoice_number || "") (b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
.toLowerCase() (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
.includes(searchText.toLowerCase()) || (b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.total || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
) )
: []; : [];
@@ -182,8 +158,8 @@ export function BillsListTableComponent({
setBillEnterContext({ setBillEnterContext({
actions: { refetch: billsQuery.refetch }, actions: { refetch: billsQuery.refetch },
context: { context: {
job, job
}, }
}); });
}} }}
> >
@@ -195,8 +171,8 @@ export function BillsListTableComponent({
actions: { refetch: billsQuery.refetch }, actions: { refetch: billsQuery.refetch },
context: { context: {
job, job,
bills: (billsQuery.data && billsQuery.data.bills) || [], bills: (billsQuery.data && billsQuery.data.bills) || []
}, }
}); });
}} }}
> >
@@ -219,7 +195,7 @@ export function BillsListTableComponent({
<Table <Table
loading={billsQuery.loading} loading={billsQuery.loading}
scroll={{ scroll={{
x: true, // y: "50rem" x: true // y: "50rem"
}} }}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
@@ -230,7 +206,4 @@ export function BillsListTableComponent({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);
mapStateToProps,
mapDispatchToProps
)(BillsListTableComponent);

View File

@@ -14,14 +14,14 @@ export default function BillsVendorsList() {
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, { const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
search: "", search: ""
}); });
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -34,22 +34,20 @@ export default function BillsVendorsList() {
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
sorter: (a, b) => alphaSort(a.name, b.name), sorter: (a, b) => alphaSort(a.name, b.name),
sortOrder: sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
}, },
{ {
title: t("vendors.fields.cost_center"), title: t("vendors.fields.cost_center"),
dataIndex: "cost_center", dataIndex: "cost_center",
key: "cost_center", key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder: sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
}, },
{ {
title: t("vendors.fields.city"), title: t("vendors.fields.city"),
dataIndex: "city", dataIndex: "city",
key: "city", key: "city"
}, }
]; ];
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
@@ -75,9 +73,7 @@ export default function BillsVendorsList() {
? data.vendors.filter( ? data.vendors.filter(
(v) => (v) =>
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) || (v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
(v.cost_center || "") (v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) ||
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.city || "").toLowerCase().includes(state.search.toLowerCase()) (v.city || "").toLowerCase().includes(state.search.toLowerCase())
) )
: (data && data.vendors) || []; : (data && data.vendors) || [];
@@ -88,12 +84,7 @@ export default function BillsVendorsList() {
title={() => { title={() => {
return ( return (
<div> <div>
<Input <Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
</div> </div>
); );
}} }}
@@ -107,13 +98,13 @@ export default function BillsVendorsList() {
handleOnRowClick(record); handleOnRowClick(record);
}, },
selectedRowKeys: [search.vendorid], selectedRowKeys: [search.vendorid],
type: "radio", type: "radio"
}} }}
onRow={(record, rowIndex) => { onRow={(record, rowIndex) => {
return { return {
onClick: (event) => { onClick: (event) => {
handleOnRowClick(record); handleOnRowClick(record);
}, // click row } // click row
}; };
}} }}
/> />

View File

@@ -13,15 +13,16 @@ import {useSplitTreatments} from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs, breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export function BreadCrumbs({ breadcrumbs, bodyshop }) { export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const {
const {treatments: {OpenSearch}} = useSplitTreatments({ treatments: { OpenSearch }
} = useSplitTreatments({
attributes: {}, attributes: {},
names: ["OpenSearch"], names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid, splitKey: bodyshop && bodyshop.imexshopid
}); });
// TODO - Client Update - Technically key is not doing anything here // TODO - Client Update - Technically key is not doing anything here
return ( return (
@@ -34,23 +35,21 @@ export function BreadCrumbs({breadcrumbs, bodyshop}) {
key: "home", key: "home",
title: ( title: (
<Link to={`/manage/`}> <Link to={`/manage/`}>
<HomeFilled/>{" "} <HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link> </Link>
), )
}, },
...breadcrumbs.map((item) => ...breadcrumbs.map((item) =>
item.link item.link
? { ? {
key: item.label, key: item.label,
title: <Link to={item.link}>{item.label}</Link>, title: <Link to={item.link}>{item.label}</Link>
} }
: { : {
key: item.label, key: item.label,
title: item.label, title: item.label
} }
), )
]} ]}
/> />
</Col> </Col>

View File

@@ -11,18 +11,14 @@ import {TemplateList} from "../../utils/TemplateConstants";
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component"; import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
caBcEtfTableModal: selectCaBcEtfTableConvert, caBcEtfTableModal: selectCaBcEtfTableConvert
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
}); });
export function ContractsFindModalContainer({ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
caBcEtfTableModal,
toggleModalVisible,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const { open } = caBcEtfTableModal; const { open } = caBcEtfTableModal;
@@ -44,7 +40,7 @@ export function ContractsFindModalContainer({
claimNumbers.push({ claimNumbers.push({
claim: trimmedShortClaim, claim: trimmedShortClaim,
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount, amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
}); });
}); });
@@ -53,8 +49,8 @@ export function ContractsFindModalContainer({
name: EtfTemplate.key, name: EtfTemplate.key,
variables: { variables: {
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`, claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
claimdata: claimNumbers, claimdata: claimNumbers
}, }
}, },
{}, {},
values.sendby === "email" ? "e" : "p" values.sendby === "email" ? "e" : "p"
@@ -78,12 +74,7 @@ export function ContractsFindModalContainer({
destroyOnClose destroyOnClose
forceRender forceRender
> >
<Form <Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
form={form}
layout="vertical"
autoComplete="no"
onFinish={handleFinish}
>
<CaBcEtfTableModalComponent form={form} /> <CaBcEtfTableModalComponent form={form} />
<Button onClick={() => form.submit()} type="primary" loading={loading}> <Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")} {t("general.labels.search")}
@@ -93,7 +84,4 @@ export function ContractsFindModalContainer({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -6,7 +6,7 @@ import {createStructuredSelector} from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export default connect(mapStateToProps, null)(PartsReceiveModalComponent); export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
@@ -20,18 +20,14 @@ export function PartsReceiveModalComponent({bodyshop, form}) {
name="table" name="table"
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<Input.TextArea rows={8} /> <Input.TextArea rows={8} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group> <Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio> <Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio> <Radio value="print">{t("general.labels.print")}</Radio>

View File

@@ -12,7 +12,7 @@ export default function CABCpvrtCalculator({disabled, form}) {
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate"); logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ form.setFieldsValue({
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2), ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2)
}); });
form.setFields([{ name: "ca_bc_pvrt", touched: true }]); form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
setVisibility(false); setVisibility(false);
@@ -36,12 +36,7 @@ export default function CABCpvrtCalculator({disabled, form}) {
); );
return ( return (
<Popover <Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
destroyTooltipOnHide
content={popContent}
open={visibility}
disabled={disabled}
>
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled /> <CalculatorFilled />
</Button> </Button>

View File

@@ -1,13 +1,13 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client"; import { useLazyQuery, useMutation } from "@apollo/client";
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd"; import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries"; 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 { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
@@ -19,21 +19,15 @@ import JobSearchSelectComponent from "../job-search-select/job-search-select.com
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation, type}) => insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
dispatch(insertAuditTrail({jobid, operation, type})), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
}); });
const CardPaymentModalComponent = ({ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
bodyshop,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail,
}) => {
const { context } = cardPaymentModal; const { context } = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -43,13 +37,10 @@ const CardPaymentModalComponent = ({
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation(); const { t } = useTranslation();
const [, {data, refetch, queryLoading}] = useLazyQuery( const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: { jobids: [context.jobid] }, variables: { jobids: [context.jobid] },
skip: true, skip: true
} });
);
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data); console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window. //Initialize the intellipay window.
@@ -79,16 +70,17 @@ const CardPaymentModalComponent = ({
declinereason: response.declinereason, declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(), ext_paymentid: response.paymentid.toString(),
successful: false, successful: false,
response, response
})), }))
}, }
}); });
payments.forEach((payment) => payments.forEach((payment) =>
insertAuditTrail({ insertAuditTrail({
jobid: payment.jobid, jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(), operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",}) type: "failedpayment"
})
); );
}); });
}; };
@@ -114,20 +106,20 @@ const CardPaymentModalComponent = ({
declinereason: values.paymentResponse.declinereason, declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(), ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true, successful: true,
response: values.paymentResponse, response: values.paymentResponse
}
]
}
}))
}, },
], refetchQueries: ["GET_JOB_BY_PK"]
},
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
}); });
toggleModalVisible(); toggleModalVisible();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification.open({ notification.open({
type: "error", type: "error",
message: t("payments.errors.inserting", {error: error.message}), message: t("payments.errors.inserting", { error: error.message })
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -148,7 +140,7 @@ const CardPaymentModalComponent = ({
try { try {
const response = await axios.post("/intellipay/lightbox_credentials", { const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop, bodyshop,
refresh: !!window.intellipay, refresh: !!window.intellipay
}); });
if (window.intellipay) { if (window.intellipay) {
@@ -167,7 +159,7 @@ const CardPaymentModalComponent = ({
} catch (error) { } catch (error) {
notification.open({ notification.open({
type: "error", type: "error",
message: t("job_payments.notifications.error.openingip"), message: t("job_payments.notifications.error.openingip")
}); });
setLoading(false); setLoading(false);
} }
@@ -181,7 +173,7 @@ const CardPaymentModalComponent = ({
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
payments: context.jobid ? [{jobid: context.jobid}] : [], payments: context.jobid ? [{ jobid: context.jobid }] : []
}} }}
> >
<Form.List name={["payments"]}> <Form.List name={["payments"]}>
@@ -198,15 +190,12 @@ const CardPaymentModalComponent = ({
name={[field.name, "jobid"]} name={[field.name, "jobid"]}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<JobSearchSelectComponent <JobSearchSelectComponent notExported={false} clm_no />
notExported={false}
clm_no
/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
@@ -216,9 +205,9 @@ const CardPaymentModalComponent = ({
name={[field.name, "amount"]} name={[field.name, "amount"]}
rules={[ rules={[
{ {
required: true, required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<CurrencyFormItemComponent /> <CurrencyFormItemComponent />
@@ -253,27 +242,21 @@ const CardPaymentModalComponent = ({
<Form.Item <Form.Item
shouldUpdate={(prevValues, curValues) => shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !== prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
curValues.payments?.map((p) => p?.jobid).join()
} }
> >
{() => { {() => {
console.log("Updating the owner info section."); console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field. //If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
if ( if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
console.log("**Calling refetch."); console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) }); refetch({ jobids: payments.map((p) => p.jobid) });
} }
console.log( console.log(
"Acc info", "Acc info",
data, data,
payments && data && data.jobs.length > 0 payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
? data.jobs.map((j) => j.ro_number).join(", ")
: null
); );
return ( return (
<> <>
@@ -282,9 +265,7 @@ const CardPaymentModalComponent = ({
data-ipayname="account" data-ipayname="account"
type="hidden" type="hidden"
value={ value={
payments && data && data.jobs.length > 0 payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
? data.jobs.map((j) => j.ro_number).join(", ")
: null
} }
/> />
<Input <Input
@@ -292,9 +273,7 @@ const CardPaymentModalComponent = ({
data-ipayname="email" data-ipayname="email"
type="hidden" type="hidden"
value={ value={
payments && data && data.jobs.length > 0 payments && data && data.jobs.length > 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
} }
/> />
</> </>
@@ -303,8 +282,7 @@ const CardPaymentModalComponent = ({
</Form.Item> </Form.Item>
<Form.Item <Form.Item
shouldUpdate={(prevValues, curValues) => shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.amount).join() !== prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join()
curValues.payments?.map((p) => p?.amount).join()
} }
> >
{() => { {() => {
@@ -315,11 +293,7 @@ const CardPaymentModalComponent = ({
return ( return (
<Space style={{ float: "right" }}> <Space style={{ float: "right" }}>
<Statistic <Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
title="Amount To Charge"
value={totalAmountToCharge}
precision={2}
/>
<Input <Input
className="ipayfield" className="ipayfield"
data-ipayname="amount" data-ipayname="amount"
@@ -351,7 +325,4 @@ const CardPaymentModalComponent = ({
); );
}; };
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);

View File

@@ -10,18 +10,14 @@ import CardPaymentModalComponent from "./card-payment-modal.component.";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
}); });
function CardPaymentModalContainer({ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
cardPaymentModal,
toggleModalVisible,
bodyshop,
}) {
const { open } = cardPaymentModal; const { open } = cardPaymentModal;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -41,7 +37,7 @@ function CardPaymentModalContainer({
footer={[ footer={[
<Button key="back" onClick={handleCancel}> <Button key="back" onClick={handleCancel}>
{t("job_payments.buttons.goback")} {t("job_payments.buttons.goback")}
</Button>, </Button>
]} ]}
width="80%" width="80%"
destroyOnClose destroyOnClose
@@ -51,7 +47,4 @@ function CardPaymentModalContainer({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer);
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalContainer);

View File

@@ -20,19 +20,16 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
try { try {
const r = await axios.post("/notifications/subscribe", { const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, { fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY, vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
}), }),
type: "messaging", type: "messaging",
imexshopid: bodyshop.imexshopid, imexshopid: bodyshop.imexshopid
}); });
console.log("FCM Topic Subscription", r.data); console.log("FCM Topic Subscription", r.data);
} catch (error) { } catch (error) {
console.log( console.log("Error attempting to subscribe to messaging topic: ", error);
"Error attempting to subscribe to messaging topic: ",
error
);
notification.open({ notification.open({
key: 'fcm', key: "fcm",
type: "warning", type: "warning",
message: t("general.errors.fcm"), message: t("general.errors.fcm"),
btn: ( btn: (
@@ -57,7 +54,7 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
{t("general.labels.help")} {t("general.labels.help")}
</Button> </Button>
</Space> </Space>
), )
}); });
} }
} }
@@ -70,7 +67,7 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
function handleMessage(payload) { function handleMessage(payload) {
FcmHandler({ FcmHandler({
client, client,
payload: (payload && payload.data && payload.data.data) || payload.data, payload: (payload && payload.data && payload.data.data) || payload.data
}); });
} }

View File

@@ -13,7 +13,7 @@ export default function ChatArchiveButton({conversation}) {
await updateConversation({ await updateConversation({
variables: { id: conversation.id, archived: !conversation.archived }, variables: { id: conversation.id, archived: !conversation.archived },
refetchQueries: ["CONVERSATION_LIST_QUERY"], refetchQueries: ["CONVERSATION_LIST_QUERY"]
}); });
setLoading(false); setLoading(false);
@@ -21,9 +21,7 @@ export default function ChatArchiveButton({conversation}) {
return ( return (
<Button onClick={handleToggleArchive} loading={loading} type="primary"> <Button onClick={handleToggleArchive} loading={loading} type="primary">
{conversation.archived {conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
? t("messaging.labels.unarchive")
: t("messaging.labels.archive")}
</Button> </Button>
); );
} }

View File

@@ -1,7 +1,7 @@
import { Badge, Card, List, Space, Tag } from "antd"; import { Badge, Card, List, Space, Tag } from "antd";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized"; import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
@@ -12,82 +12,64 @@ import _ from "lodash";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) => setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
dispatch(setSelectedConversation(conversationId)),
}); });
function ChatConversationListComponent({ function ChatConversationListComponent({
conversationList, conversationList,
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
loadMoreConversations, loadMoreConversations
}) { }) {
const cache = new CellMeasurerCache({ const cache = new CellMeasurerCache({
fixedWidth: true, fixedWidth: true,
defaultHeight: 60, defaultHeight: 60
}); });
const rowRenderer = ({ index, key, style, parent }) => { const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index]; const item = conversationList[index];
const cardContentRight = const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>; const cardContentLeft =
const cardContentLeft = item.job_conversations.length > 0 item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => ( ? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
<Tag key={idx}>{j.job.ro_number}</Tag>
))
: null; : null;
const names = <>{_.uniq(item.job_conversations.map((j, idx) => const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
OwnerNameDisplayFunction(j.job)
))}</>
const cardTitle = <> const cardTitle = (
<>
{item.label && <Tag color="blue">{item.label}</Tag>} {item.label && <Tag color="blue">{item.label}</Tag>}
{item.job_conversations.length > 0 ? ( {item.job_conversations.length > 0 ? (
<Space direction="vertical"> <Space direction="vertical">{names}</Space>
{names}
</Space>
) : ( ) : (
<Space> <Space>
<PhoneFormatter>{item.phone_num}</PhoneFormatter> <PhoneFormatter>{item.phone_num}</PhoneFormatter>
</Space> </Space>
)} )}
</> </>
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/> );
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
const getCardStyle = () => const getCardStyle = () =>
item.id === selectedConversation item.id === selectedConversation
? {backgroundColor: 'rgba(128, 128, 128, 0.2)'} ? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
: {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'}; : { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
return ( return (
<CellMeasurer <CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item <List.Item
onClick={() => setSelectedConversation(item.id)} onClick={() => setSelectedConversation(item.id)}
style={style} style={style}
className={`chat-list-item className={`chat-list-item
${ ${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
> >
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}> <Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}> <div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
{cardContentLeft} <div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
</div>
<div
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
</Card> </Card>
</List.Item> </List.Item>
</CellMeasurer> </CellMeasurer>
@@ -116,7 +98,4 @@ function ChatConversationListComponent({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationListComponent);
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -15,7 +15,7 @@ export default function ChatConversationTitleTags({jobConversations}) {
removeJobConversation({ removeJobConversation({
variables: { variables: {
conversationId: convId, conversationId: convId,
jobId: jobId, jobId: jobId
}, },
update(cache) { update(cache) {
cache.modify({ cache.modify({
@@ -23,14 +23,14 @@ export default function ChatConversationTitleTags({jobConversations}) {
fields: { fields: {
job_conversations(ex) { job_conversations(ex) {
return ex.filter((e) => e.jobid !== jobId); return ex.filter((e) => e.jobid !== jobId);
}, }
}, }
}); });
}, }
}); });
logImEXEvent("messaging_remove_job_tag", { logImEXEvent("messaging_remove_job_tag", {
conversationId: convId, conversationId: convId,
jobId: jobId, jobId: jobId
}); });
} }
}; };

View File

@@ -10,16 +10,10 @@ import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
export default function ChatConversationTitle({ conversation }) { export default function ChatConversationTitle({ conversation }) {
return ( return (
<Space wrap> <Space wrap>
<PhoneNumberFormatter> <PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} /> <ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} /> <ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags <ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
jobConversations={
(conversation && conversation.job_conversations) || []
}
/>
<ChatTagRoContainer conversation={conversation || []} /> <ChatTagRoContainer conversation={conversation || []} />
<ChatArchiveButton conversation={conversation} /> <ChatArchiveButton conversation={conversation} />
</Space> </Space>

View File

@@ -6,12 +6,7 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import "./chat-conversation.styles.scss"; import "./chat-conversation.styles.scss";
export default function ChatConversationComponent({ export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
subState,
conversation,
messages,
handleMarkConversationAsRead,
}) {
const [loading, error] = subState; const [loading, error] = subState;
if (loading) return <LoadingSkeleton />; if (loading) return <LoadingSkeleton />;

View File

@@ -2,7 +2,7 @@ import {useMutation, useQuery, useSubscription} from "@apollo/client";
import React, { useState } from "react"; import React, { useState } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries"; import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries"; import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import ChatConversationComponent from "./chat-conversation.component"; import ChatConversationComponent from "./chat-conversation.component";
@@ -11,7 +11,7 @@ import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation,
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
export default connect(mapStateToProps, null)(ChatConversationContainer); export default connect(mapStateToProps, null)(ChatConversationContainer);
@@ -20,42 +20,36 @@ export function ChatConversationContainer({bodyshop, selectedConversation}) {
const { const {
loading: convoLoading, loading: convoLoading,
error: convoError, error: convoError,
data: convoData, data: convoData
} = useQuery(GET_CONVERSATION_DETAILS, { } = useQuery(GET_CONVERSATION_DETAILS, {
variables: { conversationId: selectedConversation }, variables: { conversationId: selectedConversation },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only"
}); });
const {loading, error, data} = useSubscription( const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
CONVERSATION_SUBSCRIPTION_BY_PK, variables: { conversationId: selectedConversation }
{ });
variables: {conversationId: selectedConversation},
}
);
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
const [markConversationRead] = useMutation( const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
{
variables: { conversationId: selectedConversation }, variables: { conversationId: selectedConversation },
refetchQueries: ["UNREAD_CONVERSATION_COUNT"], refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
update(cache) { update(cache) {
cache.modify({ cache.modify({
id: cache.identify({ id: cache.identify({
__typename: "conversations", __typename: "conversations",
id: selectedConversation, id: selectedConversation
}), }),
fields: { fields: {
messages_aggregate(cached) { messages_aggregate(cached) {
return { aggregate: { count: 0 } }; return { aggregate: { count: 0 } };
},
},
});
},
} }
); }
});
}
});
const unreadCount = const unreadCount =
data && data &&
@@ -70,7 +64,7 @@ export function ChatConversationContainer({bodyshop, selectedConversation}) {
await markConversationRead({}); await markConversationRead({});
await axios.post("/sms/markConversationRead", { await axios.post("/sms/markConversationRead", {
conversationid: selectedConversation, conversationid: selectedConversation,
imexshopid: bodyshop.imexshopid, imexshopid: bodyshop.imexshopid
}); });
setMarkingAsReadInProgress(false); setMarkingAsReadInProgress(false);
} }

View File

@@ -17,13 +17,13 @@ export default function ChatLabel({conversation}) {
setLoading(true); setLoading(true);
try { try {
const response = await updateLabel({ const response = await updateLabel({
variables: {id: conversation.id, label: value}, variables: { id: conversation.id, label: value }
}); });
if (response.errors) { if (response.errors) {
notification["error"]({ notification["error"]({
message: t("messages.errors.updatinglabel", { message: t("messages.errors.updatinglabel", {
error: JSON.stringify(response.errors), error: JSON.stringify(response.errors)
}), })
}); });
} else { } else {
setEditing(false); setEditing(false);
@@ -31,8 +31,8 @@ export default function ChatLabel({conversation}) {
} catch (error) { } catch (error) {
notification["error"]({ notification["error"]({
message: t("messages.errors.updatinglabel", { message: t("messages.errors.updatinglabel", {
error: JSON.stringify(error), error: JSON.stringify(error)
}), })
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -41,13 +41,7 @@ export default function ChatLabel({conversation}) {
if (editing) { if (editing) {
return ( return (
<div> <div>
<Input <Input autoFocus value={value} onChange={(e) => setValue(e.target.value)} onBlur={handleSave} allowClear />
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={handleSave}
allowClear
/>
{loading && <Spin size="small" />} {loading && <Spin size="small" />}
</div> </div>
); );
@@ -58,10 +52,7 @@ export default function ChatLabel({conversation}) {
</Tag> </Tag>
) : ( ) : (
<Tooltip title={t("messaging.labels.addlabel")}> <Tooltip title={t("messaging.labels.addlabel")}>
<PlusOutlined <PlusOutlined style={{ cursor: "pointer" }} onClick={() => setEditing(true)} />
style={{cursor: "pointer"}}
onClick={() => setEditing(true)}
/>
</Tooltip> </Tooltip>
); );
} }

View File

@@ -9,24 +9,18 @@ import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import JobDocumentsLocalGalleryExternal import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
export function ChatMediaSelector({ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
bodyshop,
selectedMedia,
setSelectedMedia,
conversation,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -34,15 +28,10 @@ export function ChatMediaSelector({
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
jobId: jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
conversation.job_conversations[0] &&
conversation.job_conversations[0].jobid,
}, },
skip: skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
!open ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
}); });
const handleVisibleChange = (change) => { const handleVisibleChange = (change) => {
@@ -69,10 +58,7 @@ export function ChatMediaSelector({
{bodyshop.uselocalmediaserver && open && ( {bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={ jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
conversation.job_conversations[0] &&
conversation.job_conversations[0].jobid
}
/> />
)} )}
</div> </div>
@@ -81,11 +67,7 @@ export function ChatMediaSelector({
return ( return (
<Popover <Popover
content={ content={
conversation.job_conversations.length === 0 ? ( conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
<div>{t("messaging.errors.noattachedjobs")}</div>
) : (
content
)
} }
title={t("messaging.labels.selectmedia")} title={t("messaging.labels.selectmedia")}
trigger="click" trigger="click"

View File

@@ -4,7 +4,7 @@ import i18n from "i18next";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { MdDone, MdDoneAll } from "react-icons/md"; import { MdDone, MdDoneAll } from "react-icons/md";
import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized"; import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import "./chat-message-list.styles.scss"; import "./chat-message-list.styles.scss";
@@ -14,7 +14,7 @@ export default function ChatMessageListComponent({messages}) {
const _cache = new CellMeasurerCache({ const _cache = new CellMeasurerCache({
fixedWidth: true, fixedWidth: true,
// minHeight: 50, // minHeight: 50,
defaultHeight: 100, defaultHeight: 100
}); });
const scrollToBottom = (renderedrows) => { const scrollToBottom = (renderedrows) => {
@@ -35,9 +35,7 @@ export default function ChatMessageListComponent({messages}) {
ref={registerChild} ref={registerChild}
onLoad={measure} onLoad={measure}
style={style} style={style}
className={`${ className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
messages[index].isoutbound ? "mine messages" : "yours messages"
}`}
> >
<div className="message msgmargin"> <div className="message msgmargin">
{MessageRender(messages[index])} {MessageRender(messages[index])}
@@ -47,9 +45,7 @@ export default function ChatMessageListComponent({messages}) {
<div style={{ fontSize: 10 }}> <div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", { {i18n.t("messaging.labels.sentby", {
by: messages[index].userid, by: messages[index].userid,
time: dayjs(messages[index].created_at).format( time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
"MM/DD/YYYY @ hh:mm a"
),
})} })}
</div> </div>
)} )}
@@ -86,10 +82,7 @@ const MessageRender = (message) => {
<div> <div>
{message.image_path && {message.image_path &&
message.image_path.map((i, idx) => ( message.image_path.map((i, idx) => (
<div <div key={idx} style={{ display: "flex", justifyContent: "center" }}>
key={idx}
style={{display: "flex", justifyContent: "center"}}
>
<a href={i} target="__blank"> <a href={i} target="__blank">
<img alt="Received" className="message-img" src={i} /> <img alt="Received" className="message-img" src={i} />
</a> </a>

View File

@@ -5,13 +5,13 @@ import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { openChatByPhone } from "../../redux/messaging/messaging.actions"; import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
}); });
export function ChatNewConversation({ openChatByPhone }) { export function ChatNewConversation({ openChatByPhone }) {
@@ -28,10 +28,7 @@ export function ChatNewConversation({openChatByPhone}) {
<Form.Item <Form.Item
label={t("messaging.labels.phonenumber")} label={t("messaging.labels.phonenumber")}
name="phoneNumber" name="phoneNumber"
rules={[ rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "phoneNumber")]}
({getFieldValue}) =>
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
]}
> >
<PhoneFormItem /> <PhoneFormItem />
</Form.Item> </Form.Item>
@@ -49,7 +46,4 @@ export function ChatNewConversation({openChatByPhone}) {
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ChatNewConversation);
mapStateToProps,
mapDispatchToProps
)(ChatNewConversation);

View File

@@ -12,25 +12,18 @@ import {searchingForConversation} from "../../redux/messaging/messaging.selector
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation, searchingForConversation: searchingForConversation
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
}); });
export function ChatOpenButton({ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
bodyshop,
searchingForConversation,
phone,
jobid,
openChatByPhone,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
if (!phone) return <></>; if (!phone) return <></>;
if (!bodyshop.messagingservicesid) if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
return ( return (
<a <a

View File

@@ -1,13 +1,13 @@
import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons"; import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
import { useLazyQuery, useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries"; import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors"; import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component"; import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
import ChatConversationContainer from "../chat-conversation/chat-conversation.container"; import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
@@ -16,32 +16,27 @@ import "./chat-popup.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation,
chatVisible: selectChatVisible, chatVisible: selectChatVisible
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleChatVisible: () => dispatch(toggleChatVisible()), toggleChatVisible: () => dispatch(toggleChatVisible())
}); });
export function ChatPopupComponent({ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
chatVisible,
selectedConversation,
toggleChatVisible,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0); const [pollInterval, setpollInterval] = useState(0);
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
...(pollInterval > 0 ? {pollInterval} : {}), ...(pollInterval > 0 ? { pollInterval } : {})
}); });
const [getConversations, {loading, data, refetch, fetchMore}] = const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
skip: !chatVisible, skip: !chatVisible,
...(pollInterval > 0 ? {pollInterval} : {}), ...(pollInterval > 0 ? { pollInterval } : {})
}); });
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
@@ -58,8 +53,8 @@ export function ChatPopupComponent({
if (chatVisible) if (chatVisible)
getConversations({ getConversations({
variables: { variables: {
offset: 0, offset: 0
}, }
}); });
}, [chatVisible, getConversations]); }, [chatVisible, getConversations]);
@@ -67,8 +62,8 @@ export function ChatPopupComponent({
if (data) if (data)
fetchMore({ fetchMore({
variables: { variables: {
offset: data.conversations.length, offset: data.conversations.length
}, }
}); });
}, [data, fetchMore]); }, [data, fetchMore]);
@@ -80,20 +75,13 @@ export function ChatPopupComponent({
{chatVisible ? ( {chatVisible ? (
<div className="chat-popup"> <div className="chat-popup">
<Space align="center"> <Space align="center">
<Typography.Title level={4}> <Typography.Title level={4}>{t("messaging.labels.messaging")}</Typography.Title>
{t("messaging.labels.messaging")}
</Typography.Title>
<ChatNewConversation /> <ChatNewConversation />
<Tooltip title={t("messaging.labels.recentonly")}> <Tooltip title={t("messaging.labels.recentonly")}>
<InfoCircleOutlined /> <InfoCircleOutlined />
</Tooltip> </Tooltip>
<SyncOutlined <SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
style={{cursor: "pointer"}} {pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
onClick={() => refetch()}
/>
{pollInterval > 0 && (
<Tag color="yellow">{t("messaging.labels.nopush")}</Tag>
)}
</Space> </Space>
<ShrinkOutlined <ShrinkOutlined
onClick={() => toggleChatVisible()} onClick={() => toggleChatVisible()}
@@ -111,16 +99,11 @@ export function ChatPopupComponent({
/> />
)} )}
</Col> </Col>
<Col span={16}> <Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
{selectedConversation ? <ChatConversationContainer/> : null}
</Col>
</Row> </Row>
</div> </div>
) : ( ) : (
<div <div onClick={() => toggleChatVisible()} style={{ cursor: "pointer" }}>
onClick={() => toggleChatVisible()}
style={{cursor: "pointer"}}
>
<MessageOutlined className="chat-popup-info-icon" /> <MessageOutlined className="chat-popup-info-icon" />
<strong>{t("messaging.labels.messaging")}</strong> <strong>{t("messaging.labels.messaging")}</strong>
</div> </div>

View File

@@ -8,19 +8,18 @@ import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
setMessage: (message) => dispatch(setMessage(message)), 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) => ({ const items = bodyshop.md_messaging_presets.map((i, idx) => ({
key: idx, key: idx,
label: (i.label), label: i.label,
onClick: () => setMessage(i.text), onClick: () => setMessage(i.text)
})); }));
return ( return (
@@ -32,7 +31,4 @@ export function ChatPresetsComponent({bodyshop, setMessage, className}) {
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ChatPresetsComponent);
mapStateToProps,
mapDispatchToProps
)(ChatPresetsComponent);

View File

@@ -10,7 +10,7 @@ import {TemplateList} from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e))
}); });
export function ChatPrintButton({ conversation }) { export function ChatPrintButton({ conversation }) {
@@ -21,23 +21,23 @@ export function ChatPrintButton({conversation}) {
GenerateDocument( GenerateDocument(
{ {
name: TemplateList("messaging").conversation_list.key, name: TemplateList("messaging").conversation_list.key,
variables: {id: conversation.id}, variables: { id: conversation.id }
}, },
{ {
subject: TemplateList("messaging").conversation_list.subject, subject: TemplateList("messaging").conversation_list.subject
}, },
type, type,
conversation.id conversation.id
).catch(e => { ).catch((e) => {
console.warn('Something went wrong generating a document.'); console.warn("Something went wrong generating a document.");
}); });
setLoading(false); setLoading(false);
} };
return ( return (
<Space wrap> <Space wrap>
<PrinterOutlined onClick={() => generateDocument('p')}/> <PrinterOutlined onClick={() => generateDocument("p")} />
<MailOutlined onClick={() => generateDocument('e')}/> <MailOutlined onClick={() => generateDocument("e")} />
{loading && <Spin />} {loading && <Spin />}
</Space> </Space>
); );

View File

@@ -5,8 +5,8 @@ import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {sendMessage, setMessage,} from "../../redux/messaging/messaging.actions"; import { sendMessage, setMessage } from "../../redux/messaging/messaging.actions";
import {selectIsSending, selectMessage,} from "../../redux/messaging/messaging.selectors"; import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component";
@@ -14,22 +14,15 @@ import ChatPresetsComponent from "../chat-presets/chat-presets.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
isSending: selectIsSending, isSending: selectIsSending,
message: selectMessage, message: selectMessage
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
sendMessage: (message) => dispatch(sendMessage(message)), sendMessage: (message) => dispatch(sendMessage(message)),
setMessage: (message) => dispatch(setMessage(message)), setMessage: (message) => dispatch(setMessage(message))
}); });
function ChatSendMessageComponent({ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
conversation,
bodyshop,
sendMessage,
isSending,
message,
setMessage,
}) {
const inputArea = useRef(null); const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]); const [selectedMedia, setSelectedMedia] = useState([]);
useEffect(() => { useEffect(() => {
@@ -50,7 +43,7 @@ function ChatSendMessageComponent({
messagingServiceSid: bodyshop.messagingservicesid, messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id, conversationid: conversation.id,
selectedMedia: selectedImages, selectedMedia: selectedImages,
imexshopid: bodyshop.imexshopid, imexshopid: bodyshop.imexshopid
}); });
setSelectedMedia( setSelectedMedia(
selectedMedia.map((i) => { selectedMedia.map((i) => {
@@ -95,7 +88,7 @@ function ChatSendMessageComponent({
indicator={ indicator={
<LoadingOutlined <LoadingOutlined
style={{ style={{
fontSize: 24, fontSize: 24
}} }}
spin spin
/> />
@@ -105,7 +98,4 @@ function ChatSendMessageComponent({
); );
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(ChatSendMessageComponent);
mapStateToProps,
mapDispatchToProps
)(ChatSendMessageComponent);

View File

@@ -4,13 +4,7 @@ import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function ChatTagRoComponent({ export default function ChatTagRoComponent({ roOptions, loading, handleSearch, handleInsertTag, setOpen }) {
roOptions,
loading,
handleSearch,
handleInsertTag,
setOpen,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -35,11 +29,7 @@ export default function ChatTagRoComponent({
</div> </div>
{loading ? <LoadingOutlined /> : null} {loading ? <LoadingOutlined /> : null}
{loading ? ( {loading ? <LoadingOutlined /> : <CloseCircleOutlined onClick={() => setOpen(false)} />}
<LoadingOutlined/>
) : (
<CloseCircleOutlined onClick={() => setOpen(false)}/>
)}
</Space> </Space>
); );
} }

View File

@@ -27,7 +27,7 @@ export default function ChatTagRoContainer({conversation}) {
}; };
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, { const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
variables: {conversationId: conversation.id}, variables: { conversationId: conversation.id }
}); });
const handleInsertTag = (value, option) => { const handleInsertTag = (value, option) => {
@@ -37,13 +37,9 @@ export default function ChatTagRoContainer({conversation}) {
}; };
const existingJobTags = const existingJobTags =
conversation && conversation && conversation.job_conversations && conversation.job_conversations.map((i) => i.jobid);
conversation.job_conversations &&
conversation.job_conversations.map((i) => i.jobid);
const roOptions = data const roOptions = data ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id)) : [];
? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
: [];
return ( return (
<div> <div>

View File

@@ -11,9 +11,9 @@ export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
valuePropName="checked" valuePropName="checked"
rules={[ rules={[
{ {
required: required, required: required
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<Checkbox disabled={readOnly} /> <Checkbox disabled={readOnly} />

View File

@@ -9,7 +9,7 @@ const e = {
slider: Slider, slider: Slider,
text: Text, text: Text,
textarea: Textarea, textarea: Textarea,
rate: Rate, rate: Rate
}; };
export default e; export default e;

View File

@@ -10,9 +10,9 @@ export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
label={label} label={label}
rules={[ rules={[
{ {
required: required, required: required
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<Rate disabled={readOnly} allowHalf /> <Rate disabled={readOnly} allowHalf />

View File

@@ -10,9 +10,9 @@ export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
label={label} label={label}
rules={[ rules={[
{ {
required: required, required: required
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, }
]} ]}
> >
<Slider disabled={readOnly} min={min || 0} max={max || 10} /> <Slider disabled={readOnly} min={min || 0} max={max || 10} />

Some files were not shown because too many files have changed in this diff Show More