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,
useTabs: false,
tabWidth: 2,
trailingComma: 'es5',
trailingComma: "none",
semi: true,
singleQuote: false,
bracketSpacing: true,
arrowParens: 'always',
arrowParens: "always",
jsxSingleQuote: false,
bracketSameLine: false,
endOfLine: 'lf',
importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'],
endOfLine: "lf",
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderSortSpecifiers: true
};
module.exports = config;

View File

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

View File

@@ -1,8 +1,8 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
const { convertLegacyToken } = require("@ant-design/compatible/lib");
const { theme } = require("antd/lib");
const { defaultAlgorithm, defaultSeed } = theme;
@@ -13,18 +13,17 @@ const v4Token = convertLegacyToken(mapToken);
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { ...v4Token },
javascriptEnabled: true,
},
},
},
},
javascriptEnabled: true
}
}
}
}
],
webpack: {
configure: (webpackConfig) => {
@@ -33,7 +32,7 @@ module.exports = {
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
allowedHosts: "all"
},
optimization: {
...webpackConfig.optimization,
@@ -45,10 +44,10 @@ module.exports = {
}
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({
experimentalStudio: true,
env: {
FIREBASE_USERNAME: 'cypress@imex.test',
FIREBASE_PASSWORD: 'cypress',
FIREBASE_USERNAME: "cypress@imex.test",
FIREBASE_PASSWORD: "cypress"
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
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(() => {
cy.visit("/");
});
it("Renders Correctly", () => {
});
it("Renders Correctly", () => {});
it("Has the Slogan", () => {
cy.findByText("A whole x22new kind of shop management system.").should(
"exist"
);
cy.findByText("A whole x22new kind of shop management system.").should("exist");
/* ==== Generated with Cypress Studio ==== */
cy.get(
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
).click();
cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
cy.get("#email").clear();
cy.get("#email").type("patrick@imex.dev");
cy.get("#password").clear();

View File

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

View File

@@ -1,72 +1,73 @@
/// <reference types="cypress" />
context('Actions', () => {
context("Actions", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/actions')
})
cy.visit("https://example.cypress.io/commands/actions");
});
// 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
cy.get('.action-email')
.type('fake@email.com').should('have.value', 'fake@email.com')
cy.get(".action-email")
.type("fake@email.com")
.should("have.value", "fake@email.com")
// .type() with special character sequences
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
.type('{del}{selectall}{backspace}')
.type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
.type("{del}{selectall}{backspace}")
// .type() with key modifiers
.type('{alt}{option}') //these are equivalent
.type('{ctrl}{control}') //these are equivalent
.type('{meta}{command}{cmd}') //these are equivalent
.type('{shift}')
.type("{alt}{option}") //these are equivalent
.type("{ctrl}{control}") //these are equivalent
.type("{meta}{command}{cmd}") //these are equivalent
.type("{shift}")
// Delay each keypress by 0.1 sec
.type('slow.typing@email.com', {delay: 100})
.should('have.value', 'slow.typing@email.com')
.type("slow.typing@email.com", { delay: 100 })
.should("have.value", "slow.typing@email.com");
cy.get('.action-disabled')
cy.get(".action-disabled")
// Ignore error checking prior to type
// like whether the input is visible or disabled
.type('disabled error checking', {force: true})
.should('have.value', 'disabled error checking')
})
.type("disabled error checking", { force: true })
.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
cy.get('.action-focus').focus()
.should('have.class', 'focus')
.prev().should('have.attr', 'style', 'color: orange;')
})
cy.get(".action-focus").focus().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
cy.get('.action-blur').type('About to blur').blur()
.should('have.class', 'error')
.prev().should('have.attr', 'style', 'color: red;')
})
cy.get(".action-blur")
.type("About to blur")
.blur()
.should("have.class", "error")
.prev()
.should("have.attr", "style", "color: red;");
});
it('.clear() - clears an input or textarea element', () => {
it(".clear() - clears an input or textarea element", () => {
// https://on.cypress.io/clear
cy.get('.action-clear').type('Clear this text')
.should('have.value', 'Clear this text')
cy.get(".action-clear")
.type("Clear this text")
.should("have.value", "Clear this text")
.clear()
.should('have.value', '')
})
.should("have.value", "");
});
it('.submit() - submit a form', () => {
it(".submit() - submit a form", () => {
// https://on.cypress.io/submit
cy.get('.action-form')
.find('[type="text"]').type('HALFOFF')
cy.get(".action-form").find('[type="text"]').type("HALFOFF");
cy.get('.action-form').submit()
.next().should('contain', 'Your form has been submitted!')
})
cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
});
it('.click() - click on a DOM element', () => {
it(".click() - click on a DOM element", () => {
// 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:
// -----------------------------------
@@ -82,169 +83,152 @@ context('Actions', () => {
// -----------------------------------
// 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('top')
cy.get('#action-canvas').click('topRight')
cy.get('#action-canvas').click('left')
cy.get('#action-canvas').click('right')
cy.get('#action-canvas').click('bottomLeft')
cy.get('#action-canvas').click('bottom')
cy.get('#action-canvas').click('bottomRight')
cy.get("#action-canvas").click("topLeft");
cy.get("#action-canvas").click("top");
cy.get("#action-canvas").click("topRight");
cy.get("#action-canvas").click("left");
cy.get("#action-canvas").click("right");
cy.get("#action-canvas").click("bottomLeft");
cy.get("#action-canvas").click("bottom");
cy.get("#action-canvas").click("bottomRight");
// .click() accepts an x and y coordinate
// that controls where the click occurs :)
cy.get('#action-canvas')
cy.get("#action-canvas")
.click(80, 75) // click 80px on x coord and 75px on y coord
.click(170, 75)
.click(80, 165)
.click(100, 185)
.click(125, 190)
.click(150, 185)
.click(170, 165)
.click(170, 165);
// 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
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
// Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click
cy.get('.action-div').dblclick().should('not.be.visible')
cy.get('.action-input-hidden').should('be.visible')
})
cy.get(".action-div").dblclick().should("not.be.visible");
cy.get(".action-input-hidden").should("be.visible");
});
it('.rightclick() - right click on a DOM element', () => {
it(".rightclick() - right click on a DOM element", () => {
// https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
cy.get('.rightclick-action-input-hidden').should('be.visible')
})
cy.get(".rightclick-action-div").rightclick().should("not.be.visible");
cy.get(".rightclick-action-input-hidden").should("be.visible");
});
it('.check() - check a checkbox or radio element', () => {
it(".check() - check a checkbox or radio element", () => {
// https://on.cypress.io/check
// By default, .check() will check all
// matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
.check().should('be.checked')
cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
cy.get('.action-radios [type="radio"]').not('[disabled]')
.check().should('be.checked')
cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
// .check() accepts a value argument
cy.get('.action-radios [type="radio"]')
.check('radio1').should('be.checked')
cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
// .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]')
.check(['checkbox1', 'checkbox2']).should('be.checked')
cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
// Ignore error checking prior to checking
cy.get('.action-checkboxes [disabled]')
.check({force: true}).should('be.checked')
cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
cy.get('.action-radios [type="radio"]')
.check('radio3', {force: true}).should('be.checked')
})
cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
});
it('.uncheck() - uncheck a checkbox element', () => {
it(".uncheck() - uncheck a checkbox element", () => {
// https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]')
.not('[disabled]')
.uncheck().should('not.be.checked')
cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
// .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]')
.check('checkbox1')
.uncheck('checkbox1').should('not.be.checked')
cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
// .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]')
.check(['checkbox1', 'checkbox3'])
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
.check(["checkbox1", "checkbox3"])
.uncheck(["checkbox1", "checkbox3"])
.should("not.be.checked");
// Ignore error checking prior to unchecking
cy.get('.action-check [disabled]')
.uncheck({force: true}).should('not.be.checked')
})
cy.get(".action-check [disabled]").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
// at first, no option should be selected
cy.get('.action-select')
.should('have.value', '--Select a fruit--')
cy.get(".action-select").should("have.value", "--Select a fruit--");
// Select option(s) with matching text content
cy.get('.action-select').select('apples')
cy.get(".action-select").select("apples");
// confirm the apples were selected
// note that each value starts with "fr-" in our HTML
cy.get('.action-select').should('have.value', 'fr-apples')
cy.get(".action-select").should("have.value", "fr-apples");
cy.get('.action-select-multiple')
.select(['apples', 'oranges', 'bananas'])
cy.get(".action-select-multiple")
.select(["apples", "oranges", "bananas"])
// when getting multiple values, invoke "val" method first
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// 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
.should('have.value', 'fr-bananas')
.should("have.value", "fr-bananas");
cy.get('.action-select-multiple')
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
.invoke('val')
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
cy.get(".action-select-multiple")
.select(["fr-apples", "fr-oranges", "fr-bananas"])
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// assert the selected values include oranges
cy.get('.action-select-multiple')
.invoke('val').should('include', 'fr-oranges')
})
cy.get(".action-select-multiple").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
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get('#scroll-horizontal button')
.should('not.be.visible')
cy.get("#scroll-horizontal button").should("not.be.visible");
// scroll the button into view, as if the user had scrolled
cy.get('#scroll-horizontal button').scrollIntoView()
.should('be.visible')
cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
cy.get('#scroll-vertical button')
.should('not.be.visible')
cy.get("#scroll-vertical button").should("not.be.visible");
// Cypress handles the scroll direction needed
cy.get('#scroll-vertical button').scrollIntoView()
.should('be.visible')
cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
cy.get('#scroll-both button')
.should('not.be.visible')
cy.get("#scroll-both button").should("not.be.visible");
// Cypress knows to scroll to the right and down
cy.get('#scroll-both button').scrollIntoView()
.should('be.visible')
})
cy.get("#scroll-both button").scrollIntoView().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
// To interact with a range input (slider)
@@ -253,14 +237,15 @@ context('Actions', () => {
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get('.trigger-input-range')
.invoke('val', 25)
.trigger('change')
.get('input[type=range]').siblings('p')
.should('have.text', '25')
})
cy.get(".trigger-input-range")
.invoke("val", 25)
.trigger("change")
.get("input[type=range]")
.siblings("p")
.should("have.text", "25");
});
it('cy.scrollTo() - scroll the window or element to a position', () => {
it("cy.scrollTo() - scroll the window or element to a position", () => {
// https://on.cypress.io/scrollto
// You can scroll to 9 specific positions of an element:
@@ -278,22 +263,22 @@ context('Actions', () => {
// if you chain .scrollTo() off of cy, we will
// 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:
// (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
// 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')
cy.get('#scrollable-vertical').scrollTo('center', {easing: 'linear'})
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
// 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" />
context('Aliasing', () => {
context("Aliasing", () => {
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
// Alias a DOM element for use later
// We don't have to traverse to the element
// later in our code, we reference it with @
cy.get('.as-table').find('tbody>tr')
.first().find('td').first()
.find('button').as('firstBtn')
cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
// when we reference the alias, we place an
// @ in front of its name
cy.get('@firstBtn').click()
cy.get("@firstBtn").click();
cy.get('@firstBtn')
.should('have.class', 'btn-success')
.and('contain', 'Changed')
})
cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
});
it('.as() - alias a route for later use', () => {
it(".as() - alias a route for later use", () => {
// 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
// the button is clicked in scripts.js
cy.get('.network-btn').click()
cy.get(".network-btn").click();
// 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" />
context('Assertions', () => {
context("Assertions", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/assertions')
})
cy.visit("https://example.cypress.io/commands/assertions");
});
describe('Implicit Assertions', () => {
it('.should() - make an assertion about the current subject', () => {
describe("Implicit Assertions", () => {
it(".should() - make an assertion about the current subject", () => {
// https://on.cypress.io/should
cy.get('.assertion-table')
.find('tbody tr:last')
.should('have.class', 'success')
.find('td')
cy.get(".assertion-table")
.find("tbody tr:last")
.should("have.class", "success")
.find("td")
.first()
// checking the text of the <td> element in various ways
.should('have.text', 'Column content')
.should('contain', 'Column content')
.should('have.html', 'Column content')
.should("have.text", "Column content")
.should("contain", "Column content")
.should("have.html", "Column content")
// chai-jquery uses "is()" to check if element matches selector
.should('match', 'td')
.should("match", "td")
// to match text content against a regular expression
// first need to invoke jQuery method text()
// and then match using regular expression
.invoke('text')
.should('match', /column content/i)
.invoke("text")
.should("match", /column content/i);
// a better way to check element's text content against a regular expression
// is to use "cy.contains"
// https://on.cypress.io/contains
cy.get('.assertion-table')
.find('tbody tr:last')
cy.get(".assertion-table")
.find("tbody tr:last")
// finds first <td> element with text content matching regular expression
.contains('td', /column content/i)
.should('be.visible')
.contains("td", /column content/i)
.should("be.visible");
// for more information about asserting element's text
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-elements-text-contents
})
});
it('.and() - chain multiple assertions together', () => {
it(".and() - chain multiple assertions together", () => {
// https://on.cypress.io/and
cy.get('.assertions-link')
.should('have.class', 'active')
.and('have.attr', 'href')
.and('include', 'cypress.io')
})
})
cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
});
});
describe('Explicit Assertions', () => {
describe("Explicit 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
expect(true).to.be.true
const o = {foo: 'bar'}
expect(true).to.be.true;
const o = { foo: "bar" };
expect(o).to.equal(o)
expect(o).to.deep.equal({foo: 'bar'})
expect(o).to.equal(o);
expect(o).to.deep.equal({ foo: "bar" });
// matching text using regular expression
expect('FooBar').to.match(/bar$/i)
})
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
// of explicit assertions within it.
// The ".should(cb)" function will be retried
// automatically until it passes all your explicit assertions or times out.
cy.get('.assertions-p')
.find('p')
cy.get(".assertions-p")
.find("p")
.should(($p) => {
// https://on.cypress.io/$
// return an array of texts from all of the p's
// @ts-ignore TS6133 unused variable
const texts = $p.map((i, el) => Cypress.$(el).text())
const texts = $p.map((i, el) => Cypress.$(el).text());
// jquery map returns jquery object
// and .get() convert this to simple array
const paragraphs = texts.get()
const paragraphs = texts.get();
// 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
// message with each assertion
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
'Some text from first p',
'More text from second p',
'And even more text from third p',
])
})
})
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
"Some text from first p",
"More text from second p",
"And even more text from third p"
]);
});
});
it('finds element by class name regex', () => {
cy.get('.docs-header')
.find('div')
it("finds element by class name regex", () => {
cy.get(".docs-header")
.find("div")
// .should(cb) callback function will be retried
.should(($div) => {
expect($div).to.have.length(1)
expect($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,
// it either passes or fails
.then(($div) => {
expect($div, 'text content').to.have.text('Introduction')
})
})
expect($div, "text content").to.have.text("Introduction");
});
});
it('can throw any error', () => {
cy.get('.docs-header')
.find('div')
it("can throw any error", () => {
cy.get(".docs-header")
.find("div")
.should(($div) => {
if ($div.length !== 1) {
// you can throw your own errors
throw new Error('Did not find 1 element')
throw new Error("Did not find 1 element");
}
const className = $div[0].className
const className = $div[0].className;
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.
* @type {string}
*/
let text
let text;
/**
* Normalizes passed text,
* useful before comparing text with spaces and different capitalization.
* @param {string} s Text to normalize
*/
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
cy.get('.two-elements')
.find('.first')
cy.get(".two-elements")
.find(".first")
.then(($first) => {
// save text from the first element
text = normalizeText($first.text())
})
text = normalizeText($first.text());
});
cy.get('.two-elements')
.find('.second')
cy.get(".two-elements")
.find(".second")
.should(($div) => {
// 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 = {
name: 'Joe',
age: 20,
}
name: "Joe",
age: 20
};
assert.isObject(person, 'value is object')
})
assert.isObject(person, "value is object");
});
it('retries the should callback until assertions pass', () => {
cy.get('#random-number')
.should(($div) => {
const n = parseFloat($div.text())
it("retries the should callback until assertions pass", () => {
cy.get("#random-number").should(($div) => {
const n = parseFloat($div.text());
expect(n).to.be.gte(1).and.be.lte(10)
})
})
})
})
expect(n).to.be.gte(1).and.be.lte(10);
});
});
});
});

View File

@@ -1,97 +1,96 @@
/// <reference types="cypress" />
context('Connectors', () => {
context("Connectors", () => {
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
cy.get('.connectors-each-ul>li')
.each(($el, index, $list) => {
console.log($el, index, $list)
})
})
cy.get(".connectors-each-ul>li").each(($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
cy.get('.connectors-its-ul>li')
cy.get(".connectors-its-ul>li")
// calls the 'length' property yielding that value
.its('length')
.should('be.gt', 2)
})
.its("length")
.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
// $('.connectors-div').hide()
// 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'
.invoke('show')
.should('be.visible')
})
.invoke("show")
.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
const arr = ['foo', 'bar', 'baz']
const arr = ["foo", "bar", "baz"];
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.eq('foo')
expect(bar).to.eq('bar')
expect(baz).to.eq('baz')
})
})
expect(foo).to.eq("foo");
expect(bar).to.eq("bar");
expect(baz).to.eq("baz");
});
});
describe('.then()', () => {
it('invokes a callback function with the current subject', () => {
describe(".then()", () => {
it("invokes a callback function with the current subject", () => {
// https://on.cypress.io/then
cy.get('.connectors-list > li')
.then(($lis) => {
expect($lis, '3 items').to.have.length(3)
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
})
})
cy.get(".connectors-list > li").then(($lis) => {
expect($lis, "3 items").to.have.length(3);
expect($lis.eq(0), "first item").to.contain("Walk the dog");
expect($lis.eq(1), "second item").to.contain("Feed the cat");
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
});
});
it('yields the returned value to the next command', () => {
it("yields the returned value to the next command", () => {
cy.wrap(1)
.then((num) => {
expect(num).to.equal(1)
expect(num).to.equal(1);
return 2
return 2;
})
.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)
.then((num) => {
expect(num).to.equal(1)
expect(num).to.equal(1);
// note that nothing is returned from this callback
})
.then((num) => {
// this callback receives the original unchanged value 1
expect(num).to.equal(1)
})
})
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)
.then((num) => {
expect(num).to.equal(1)
expect(num).to.equal(1);
// note how we run a Cypress command
// the result yielded by this Cypress command
// will be passed to the second ".then"
cy.wrap(2)
cy.wrap(2);
})
.then((num) => {
// 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" />
context('Cookies', () => {
context("Cookies", () => {
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
// 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
cy.get('#getCookie .set-a-cookie').click()
cy.get("#getCookie .set-a-cookie").click();
// 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
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().should('have.length', 1).should((cookies) => {
cy.getCookies()
.should("have.length", 1)
.should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
})
expect(cookies[0]).to.have.property("name", "token");
expect(cookies[0]).to.have.property("value", "123ABC");
expect(cookies[0]).to.have.property("httpOnly", false);
expect(cookies[0]).to.have.property("secure", false);
expect(cookies[0]).to.have.property("domain");
expect(cookies[0]).to.have.property("path");
});
});
it('cy.setCookie() - set a browser cookie', () => {
it("cy.setCookie() - set a browser cookie", () => {
// 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('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
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.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
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()
cy.clearCookies();
cy.getCookies().should('be.empty')
})
})
cy.getCookies().should("be.empty");
});
});

View File

@@ -1,202 +1,208 @@
/// <reference types="cypress" />
context('Cypress.Commands', () => {
context("Cypress.Commands", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/custom-commands
it('.add() - create a custom command', () => {
Cypress.Commands.add('console', {
prevSubject: true,
}, (subject, method) => {
it(".add() - create a custom command", () => {
Cypress.Commands.add(
"console",
{
prevSubject: true
},
(subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
method = method || "log";
// log the subject to the console
// @ts-ignore TS7017
console[method]('The subject is', subject)
console[method]("The subject is", subject);
// whatever we return becomes the new subject
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
return subject;
}
);
// @ts-ignore TS2339
cy.get('button').console('info').then(($button) => {
cy.get("button")
.console("info")
.then(($button) => {
// subject is still $button
})
})
})
});
});
});
context('Cypress.Cookies', () => {
context("Cypress.Cookies", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/cookies
it('.debug() - enable or disable debugging', () => {
Cypress.Cookies.debug(true)
it(".debug() - enable or disable debugging", () => {
Cypress.Cookies.debug(true);
// Cypress will now log in the console when
// cookies are set or cleared
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
cy.clearCookie('fakeCookie')
cy.setCookie('fakeCookie', '123ABC')
})
cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie("fakeCookie");
cy.setCookie("fakeCookie", "123ABC");
cy.clearCookie("fakeCookie");
cy.setCookie("fakeCookie", "123ABC");
});
it('.preserveOnce() - preserve cookies by key', () => {
it(".preserveOnce() - preserve cookies by key", () => {
// 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
// the next test starts
cy.setCookie('lastCookie', '789XYZ')
Cypress.Cookies.preserveOnce('lastCookie')
})
cy.setCookie("lastCookie", "789XYZ");
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
// not be cleared before each new test runs
Cypress.Cookies.defaults({
preserve: 'session_id',
})
})
})
preserve: "session_id"
});
});
});
context('Cypress.arch', () => {
context("Cypress.arch", () => {
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
expect(Cypress.arch).to.exist
})
})
expect(Cypress.arch).to.exist;
});
});
context('Cypress.config()', () => {
context("Cypress.config()", () => {
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
let myConfig = Cypress.config()
let myConfig = Cypress.config();
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
expect(myConfig).to.have.property('baseUrl', null)
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
expect(myConfig).to.have.property('requestTimeout', 5000)
expect(myConfig).to.have.property('responseTimeout', 30000)
expect(myConfig).to.have.property('viewportHeight', 660)
expect(myConfig).to.have.property('viewportWidth', 1000)
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
expect(myConfig).to.have.property('waitForAnimations', true)
expect(myConfig).to.have.property("animationDistanceThreshold", 5);
expect(myConfig).to.have.property("baseUrl", null);
expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
expect(myConfig).to.have.property("requestTimeout", 5000);
expect(myConfig).to.have.property("responseTimeout", 30000);
expect(myConfig).to.have.property("viewportHeight", 660);
expect(myConfig).to.have.property("viewportWidth", 1000);
expect(myConfig).to.have.property("pageLoadTimeout", 60000);
expect(myConfig).to.have.property("waitForAnimations", true);
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
// this will change the config for the rest of your tests!
Cypress.config('pageLoadTimeout', 20000)
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(() => {
cy.visit('https://example.cypress.io/cypress-api')
})
cy.visit("https://example.cypress.io/cypress-api");
});
// https://on.cypress.io/dom
it('.isHidden() - determine if a DOM element is hidden', () => {
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
let visibleP = Cypress.$('.dom-p p.visible').get(0)
it(".isHidden() - determine if a DOM element is hidden", () => {
let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
let visibleP = Cypress.$(".dom-p p.visible").get(0);
// our first paragraph has css class 'hidden'
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
expect(Cypress.dom.isHidden(visibleP)).to.be.false
})
})
expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
expect(Cypress.dom.isHidden(visibleP)).to.be.false;
});
});
context('Cypress.env()', () => {
context("Cypress.env()", () => {
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
// https://on.cypress.io/environment-variables
it('Get environment variables', () => {
it("Get environment variables", () => {
// https://on.cypress.io/env
// set multiple environment variables
Cypress.env({
host: 'veronica.dev.local',
api_server: 'http://localhost:8888/v1/',
})
host: "veronica.dev.local",
api_server: "http://localhost:8888/v1/"
});
// get environment variable
expect(Cypress.env('host')).to.eq('veronica.dev.local')
expect(Cypress.env("host")).to.eq("veronica.dev.local");
// set environment variable
Cypress.env('api_server', 'http://localhost:8888/v2/')
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
Cypress.env("api_server", "http://localhost:8888/v2/");
expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
// get all environment variable
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
})
})
expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
});
});
context('Cypress.log', () => {
context("Cypress.log", () => {
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
})
})
});
});
context('Cypress.platform', () => {
context("Cypress.platform", () => {
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
expect(Cypress.platform).to.be.exist
})
})
expect(Cypress.platform).to.be.exist;
});
});
context('Cypress.version', () => {
context("Cypress.version", () => {
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
expect(Cypress.version).to.be.exist
})
})
expect(Cypress.version).to.be.exist;
});
});
context('Cypress.spec', () => {
context("Cypress.spec", () => {
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
// 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
// the built-in JavaScript bundler
// @ts-ignore
const requiredExample = require('../../fixtures/example')
const requiredExample = require("../../fixtures/example");
context('Files', () => {
context("Files", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/files')
})
cy.visit("https://example.cypress.io/commands/files");
});
beforeEach(() => {
// load example.json fixture file and store
// in the test context object
cy.fixture('example.json').as('example')
})
cy.fixture("example.json").as("example");
});
it('cy.fixture() - load a fixture', () => {
it("cy.fixture() - load a fixture", () => {
// https://on.cypress.io/fixture
// Instead of writing a response inline you can
@@ -24,65 +24,63 @@ context('Files', () => {
// when application makes an Ajax request matching "GET **/comments/*"
// Cypress will intercept it and reply with the object in `example.json` fixture
cy.intercept('GET', '**/comments/*', {fixture: 'example.json'}).as('getComment')
cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
// we have code that gets a comment when
// the button is clicked in scripts.js
cy.get('.fixture-btn').click()
cy.get(".fixture-btn").click();
cy.wait('@getComment').its('response.body')
.should('have.property', 'name')
.and('include', 'Using fixtures to represent data')
})
cy.wait("@getComment")
.its("response.body")
.should("have.property", "name")
.and("include", "Using fixtures to represent data");
});
it('cy.fixture() or require - load a fixture', function () {
it("cy.fixture() or require - load a fixture", function () {
// we are inside the "function () { ... }"
// callback and can use test context object "this"
// "this.example" was loaded in "beforeEach" function callback
expect(this.example, 'fixture in the test context')
.to.deep.equal(requiredExample)
expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
cy.wrap(this.example)
.should('deep.equal', requiredExample)
})
cy.wrap(this.example).should("deep.equal", requiredExample);
});
it('cy.readFile() - read file contents', () => {
it("cy.readFile() - read file contents", () => {
// https://on.cypress.io/readfile
// You can read a file and yield its contents
// The filePath is relative to your project's root.
cy.readFile('cypress.json').then((json) => {
expect(json).to.be.an('object')
})
})
cy.readFile("cypress.json").then((json) => {
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
// You can write to a file
// Use a response from a request to automatically
// generate a fixture file for use later
cy.request('https://jsonplaceholder.cypress.io/users')
.then((response) => {
cy.writeFile('cypress/fixtures/users.json', response.body)
})
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
cy.writeFile("cypress/fixtures/users.json", response.body);
});
cy.fixture('users').should((users) => {
expect(users[0].name).to.exist
})
cy.fixture("users").should((users) => {
expect(users[0].name).to.exist;
});
// JavaScript arrays and objects are stringified
// and formatted into text.
cy.writeFile('cypress/fixtures/profile.json', {
cy.writeFile("cypress/fixtures/profile.json", {
id: 8739,
name: 'Jane',
email: 'jane@example.com',
})
name: "Jane",
email: "jane@example.com"
});
cy.fixture('profile').should((profile) => {
expect(profile.name).to.eq('Jane')
})
})
})
cy.fixture("profile").should((profile) => {
expect(profile.name).to.eq("Jane");
});
});
});

View File

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

View File

@@ -1,32 +1,32 @@
/// <reference types="cypress" />
context('Location', () => {
context("Location", () => {
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
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
cy.location().should((location) => {
expect(location.hash).to.be.empty
expect(location.href).to.eq('https://example.cypress.io/commands/location')
expect(location.host).to.eq('example.cypress.io')
expect(location.hostname).to.eq('example.cypress.io')
expect(location.origin).to.eq('https://example.cypress.io')
expect(location.pathname).to.eq('/commands/location')
expect(location.port).to.eq('')
expect(location.protocol).to.eq('https:')
expect(location.search).to.be.empty
})
})
expect(location.hash).to.be.empty;
expect(location.href).to.eq("https://example.cypress.io/commands/location");
expect(location.host).to.eq("example.cypress.io");
expect(location.hostname).to.eq("example.cypress.io");
expect(location.origin).to.eq("https://example.cypress.io");
expect(location.pathname).to.eq("/commands/location");
expect(location.port).to.eq("");
expect(location.protocol).to.eq("https:");
expect(location.search).to.be.empty;
});
});
it('cy.url() - get the current URL', () => {
it("cy.url() - get the current 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" />
context('Misc', () => {
context("Misc", () => {
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
// cy.end is useful when you want to end a chain of commands
// and force Cypress to re-query from the root element
cy.get('.misc-table').within(() => {
cy.get(".misc-table").within(() => {
// ends the current chain and yields null
cy.contains('Cheryl').click().end()
cy.contains("Cheryl").click().end();
// 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.
// so you can take actions necessary for
// your test outside the scope of Cypress.
@@ -28,79 +28,71 @@ context('Misc', () => {
// we can use Cypress.platform string to
// select appropriate command
// 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
// https://github.com/cypress-io/cypress/issues/5169
// so skip some of the tests by passing flag "--env circle=true"
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
if (isCircleOnWindows) {
cy.log('Skipping test on CircleCI')
cy.log("Skipping test on CircleCI");
return
return;
}
// cy.exec problem on Shippable CI
// https://github.com/cypress-io/cypress/issues/6718
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
if (isShippable) {
cy.log('Skipping test on ShippableCI')
cy.log("Skipping test on ShippableCI");
return
return;
}
cy.exec('echo Jane Lane')
.its('stdout').should('contain', 'Jane Lane')
cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
if (Cypress.platform === 'win32') {
cy.exec('print cypress.json')
.its('stderr').should('be.empty')
if (Cypress.platform === "win32") {
cy.exec("print cypress.json").its("stderr").should("be.empty");
} else {
cy.exec('cat cypress.json')
.its('stderr').should('be.empty')
cy.exec("cat cypress.json").its("stderr").should("be.empty");
cy.exec('pwd')
.its('code').should('eq', 0)
cy.exec("pwd").its("code").should("eq", 0);
}
})
});
it('cy.focused() - get the DOM element that has focus', () => {
it("cy.focused() - get the DOM element that has focus", () => {
// https://on.cypress.io/focused
cy.get('.misc-form').find('#name').click()
cy.focused().should('have.id', 'name')
cy.get(".misc-form").find("#name").click();
cy.focused().should("have.id", "name");
cy.get('.misc-form').find('#description').click()
cy.focused().should('have.id', 'description')
})
cy.get(".misc-form").find("#description").click();
cy.focused().should("have.id", "description");
});
context('Cypress.Screenshot', function () {
it('cy.screenshot() - take a screenshot', () => {
context("Cypress.Screenshot", function () {
it("cy.screenshot() - take a 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({
blackout: ['.foo'],
capture: 'viewport',
blackout: [".foo"],
capture: "viewport",
clip: { x: 0, y: 0, width: 200, height: 200 },
scale: false,
disableTimersAndAnimations: true,
screenshotOnRunFailure: true,
onBeforeScreenshot() {
},
onAfterScreenshot() {
},
})
})
})
onBeforeScreenshot() {},
onAfterScreenshot() {}
});
});
});
it('cy.wrap() - wrap an object', () => {
it("cy.wrap() - wrap an object", () => {
// https://on.cypress.io/wrap
cy.wrap({foo: 'bar'})
.should('have.property', 'foo')
.and('include', 'bar')
})
})
cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
});
});

View File

@@ -1,56 +1,56 @@
/// <reference types="cypress" />
context('Navigation', () => {
context("Navigation", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io')
cy.get('.navbar-nav').contains('Commands').click()
cy.get('.dropdown-menu').contains('Navigation').click()
})
cy.visit("https://example.cypress.io");
cy.get(".navbar-nav").contains("Commands").click();
cy.get(".dropdown-menu").contains("Navigation").click();
});
it('cy.go() - go back or forward in the browser\'s history', () => {
it("cy.go() - go back or forward in the browser's history", () => {
// https://on.cypress.io/go
cy.location('pathname').should('include', 'navigation')
cy.location("pathname").should("include", "navigation");
cy.go('back')
cy.location('pathname').should('not.include', 'navigation')
cy.go("back");
cy.location("pathname").should("not.include", "navigation");
cy.go('forward')
cy.location('pathname').should('include', 'navigation')
cy.go("forward");
cy.location("pathname").should("include", "navigation");
// clicking back
cy.go(-1)
cy.location('pathname').should('not.include', 'navigation')
cy.go(-1);
cy.location("pathname").should("not.include", "navigation");
// clicking forward
cy.go(1)
cy.location('pathname').should('include', 'navigation')
})
cy.go(1);
cy.location("pathname").should("include", "navigation");
});
it('cy.reload() - reload the page', () => {
it("cy.reload() - reload the page", () => {
// https://on.cypress.io/reload
cy.reload()
cy.reload();
// 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
// Visit any sub-domain of your current domain
// 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
onBeforeLoad(contentWindow) {
// contentWindow is the remote page's window object
expect(typeof contentWindow === 'object').to.be.true
expect(typeof contentWindow === "object").to.be.true;
},
onLoad(contentWindow) {
// 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" />
context('Network Requests', () => {
context("Network Requests", () => {
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
it('cy.request() - make an XHR request', () => {
it("cy.request() - make an XHR request", () => {
// https://on.cypress.io/request
cy.request('https://jsonplaceholder.cypress.io/comments')
.should((response) => {
expect(response.status).to.eq(200)
cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
expect(response.status).to.eq(200);
// the server sometimes gets an extra comment posted from another machine
// which gets returned as 1 extra object
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.have.property("headers");
expect(response).to.have.property("duration");
});
});
it('cy.request() - verify response using BDD syntax', () => {
cy.request('https://jsonplaceholder.cypress.io/comments')
.then((response) => {
it("cy.request() - verify response using BDD syntax", () => {
cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
// https://on.cypress.io/assertions
expect(response).property('status').to.equal(200)
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
expect(response).to.include.keys('headers', 'duration')
})
})
expect(response).property("status").to.equal(200);
expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
expect(response).to.include.keys("headers", "duration");
});
});
it('cy.request() with query parameters', () => {
it("cy.request() with query parameters", () => {
// will execute request
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
cy.request({
url: 'https://jsonplaceholder.cypress.io/comments',
url: "https://jsonplaceholder.cypress.io/comments",
qs: {
postId: 1,
id: 3,
},
id: 3
}
})
.its('body')
.should('be.an', 'array')
.and('have.length', 1)
.its('0') // yields first element of the array
.should('contain', {
.its("body")
.should("be.an", "array")
.and("have.length", 1)
.its("0") // yields first element of the array
.should("contain", {
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
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body') // yields the response object
.its('0') // yields the first element of the returned list
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its("body") // yields the response object
.its("0") // yields the first element of the returned list
// the above two commands its('body').its('0')
// can be written as its('body.0')
// if you do not care about TypeScript checks
.then((user) => {
expect(user).property('id').to.be.a('number')
expect(user).property("id").to.be.a("number");
// make a new post on behalf of the user
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
})
title: "Cypress Test Runner",
body: "Fast, easy and reliable testing for anything that runs in a browser."
});
})
// note that the value here is the returned value of the 2nd request
// which is the new post object
.then((response) => {
expect(response).property('status').to.equal(201) // new entity created
expect(response).property('body').to.contain({
title: 'Cypress Test Runner',
})
expect(response).property("status").to.equal(201); // new entity created
expect(response).property("body").to.contain({
title: "Cypress Test Runner"
});
// we don't know the exact post id - only that it will be > 100
// since JSONPlaceholder has built-in 100 posts
expect(response.body).property('id').to.be.a('number')
.and.to.be.gt(100)
expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
// we don't know the user id here - since it was in above closure
// so in this test just confirm that the property is there
expect(response.body).property('userId').to.be.a('number')
})
})
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
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
.its('body').its('0') // yields the first element of the returned list
.as('user') // saves the object in the test context
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
.its("body")
.its("0") // yields the first element of the returned list
.as("user") // saves the object in the test context
.then(function () {
// NOTE 👀
// By the time this callback runs the "as('user')" command
@@ -98,66 +96,70 @@ context('Network Requests', () => {
// To access the test context we need to use
// the "function () { ... }" callback form,
// otherwise "this" points at a wrong or undefined object!
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
userId: this.user.id,
title: 'Cypress Test Runner',
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
title: "Cypress Test Runner",
body: "Fast, easy and reliable testing for anything that runs in a browser."
})
.its('body').as('post') // save the new post from the response
.its("body")
.as("post"); // save the new post from the response
})
.then(function () {
// When this callback runs, both "cy.request" API commands have finished
// and the test context has "user" and "post" objects set.
// Let's verify them.
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
})
})
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
let message = 'whoa, this comment does not exist'
let message = "whoa, this comment does not exist";
// 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
// the button is clicked in scripts.js
cy.get('.network-btn').click()
cy.get(".network-btn").click();
// 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
cy.intercept('POST', '**/comments').as('postComment')
cy.intercept("POST", "**/comments").as("postComment");
// we have code that posts a comment when
// the button is clicked in scripts.js
cy.get('.network-post').click()
cy.wait('@postComment').should(({request, response}) => {
expect(request.body).to.include('email')
expect(request.headers).to.have.property('content-type')
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
})
cy.get(".network-post").click();
cy.wait("@postComment").should(({ request, response }) => {
expect(request.body).to.include("email");
expect(request.headers).to.have.property("content-type");
expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
});
// Stub a response to PUT comments/ ****
cy.intercept({
method: 'PUT',
url: '**/comments/*',
}, {
cy.intercept(
{
method: "PUT",
url: "**/comments/*"
},
{
statusCode: 404,
body: { error: message },
headers: {'access-control-allow-origin': '*'},
delayMs: 500,
}).as('putComment')
headers: { "access-control-allow-origin": "*" },
delayMs: 500
}
).as("putComment");
// we have code that puts a comment when
// the button is clicked in scripts.js
cy.get('.network-put').click()
cy.get(".network-put").click();
cy.wait('@putComment')
cy.wait("@putComment");
// 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" />
context('Querying', () => {
context("Querying", () => {
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
// think of this like the '$' in jQuery
it('cy.get() - query DOM elements', () => {
it("cy.get() - query DOM elements", () => {
// https://on.cypress.io/get
cy.get('#query-btn').should('contain', 'Button')
cy.get("#query-btn").should("contain", "Button");
cy.get('.query-btn').should('contain', 'Button')
cy.get(".query-btn").should("contain", "Button");
cy.get('#querying .well>button:first').should('contain', 'Button')
cy.get("#querying .well>button:first").should("contain", "Button");
// ↲
// Use CSS selectors just like jQuery
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
cy.get('[data-test-id="test-example"]').should("have.class", "example");
// 'cy.get()' yields jQuery object, you can get its attribute
// by invoking `.attr()` method
cy.get('[data-test-id="test-example"]')
.invoke('attr', 'data-test-id')
.should('equal', 'test-example')
cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
// or you can get element's CSS property
cy.get('[data-test-id="test-example"]')
.invoke('css', 'position')
.should('equal', 'static')
cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
// or use assertions directly during 'cy.get()'
// https://on.cypress.io/assertions
cy.get('[data-test-id="test-example"]')
.should('have.attr', 'data-test-id', 'test-example')
.and('have.css', 'position', 'static')
})
.should("have.attr", "data-test-id", "test-example")
.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
cy.get('.query-list')
.contains('bananas')
.should('have.class', 'third')
cy.get(".query-list").contains("bananas").should("have.class", "third");
// we can pass a regexp to `.contains()`
cy.get('.query-list')
.contains(/^b\w+/)
.should('have.class', 'third')
cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
cy.get('.query-list')
.contains('apples')
.should('have.class', 'first')
cy.get(".query-list").contains("apples").should("have.class", "first");
// passing a selector to contains will
// yield the selector containing the text
cy.get('#querying')
.contains('ul', 'oranges')
.should('have.class', 'query-list')
cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
cy.get('.query-button')
.contains('Save Form')
.should('have.class', 'btn')
})
cy.get(".query-button").contains("Save Form").should("have.class", "btn");
});
it('.within() - query DOM elements within a specific element', () => {
it(".within() - query DOM elements within a specific element", () => {
// https://on.cypress.io/within
cy.get('.query-form').within(() => {
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
})
})
cy.get(".query-form").within(() => {
cy.get("input:first").should("have.attr", "placeholder", "Email");
cy.get("input:last").should("have.attr", "placeholder", "Password");
});
});
it('cy.root() - query the root DOM element', () => {
it("cy.root() - query the root DOM element", () => {
// https://on.cypress.io/root
// 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
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
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
// Worst - too generic, no context
cy.get('button').click()
cy.get("button").click();
// 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.
cy.get('[name=submission]').click()
cy.get("[name=submission]").click();
// 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
// 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.
cy.contains('Submit').click()
cy.contains("Submit").click();
// 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
// https://github.com/cypress-io/cypress/issues/6720
context('Spies, Stubs, and Clock', () => {
it('cy.spy() - wrap a method in a spy', () => {
context("Spies, Stubs, and Clock", () => {
it("cy.spy() - wrap a method in a spy", () => {
// https://on.cypress.io/spy
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
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', () => {
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
it("cy.spy() retries until assertions pass", () => {
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
@@ -28,26 +27,26 @@ context('Spies, Stubs, and Clock', () => {
* @param x {any}
*/
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(() => {
obj.foo('first')
}, 500)
obj.foo("first");
}, 500);
setTimeout(() => {
obj.foo('second')
}, 2500)
obj.foo("second");
}, 2500);
cy.get('@foo').should('have.been.calledTwice')
})
cy.get("@foo").should("have.been.calledTwice");
});
it('cy.stub() - create a stub and/or replace a function with stub', () => {
it("cy.stub() - create a stub and/or replace a function with stub", () => {
// https://on.cypress.io/stub
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
const obj = {
/**
@@ -56,48 +55,45 @@ context('Spies, Stubs, and Clock', () => {
* @param b {string}
*/
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
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#clock-div').click()
.should('have.text', '1489449600')
})
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#clock-div").click().should("have.text", "1489449600");
});
it('cy.tick() - move time in the browser', () => {
it("cy.tick() - move time in the browser", () => {
// https://on.cypress.io/tick
// create the date in UTC so its always the same
// no matter what local timezone the browser is running in
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
cy.clock(now)
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
cy.get('#tick-div').click()
.should('have.text', '1489449600')
cy.clock(now);
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
cy.get("#tick-div").click().should("have.text", "1489449600");
cy.tick(10000) // 10 seconds passed
cy.get('#tick-div').click()
.should('have.text', '1489449610')
})
cy.tick(10000); // 10 seconds passed
cy.get("#tick-div").click().should("have.text", "1489449610");
});
it('cy.stub() matches depending on arguments', () => {
it("cy.stub() matches depending on arguments", () => {
// see all possible matchers at
// https://sinonjs.org/releases/latest/matchers/
const greeter = {
@@ -106,26 +102,28 @@ context('Spies, Stubs, and Clock', () => {
* @param {string} 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
.withArgs(Cypress.sinon.match.string).returns('Hi')
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
.withArgs(Cypress.sinon.match.string)
.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
expect(() => greeter.greet(42)).to.throw('Invalid name')
expect(greeter.greet).to.have.been.calledTwice
expect(() => greeter.greet(42)).to.throw("Invalid name");
expect(greeter.greet).to.have.been.calledTwice;
// non-matched calls goes the actual method
// @ts-ignore
expect(greeter.greet()).to.equal('Hello, undefined!')
})
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
// https://sinonjs.org/releases/latest/matchers/
const calculator = {
@@ -135,72 +133,71 @@ context('Spies, Stubs, and Clock', () => {
* @param b {number}
*/
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
expect(spy).to.be.calledWith(2, 3)
expect(spy).to.be.calledWith(2, 3);
// let's confirm "add" method was called with two numbers
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
// alternatively, provide the value to match
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
// match any value
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
// match any value from a list
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
/**
* Returns true if the given number is event
* @param {number} x
*/
const isEven = (x) => x % 2 === 0
const isEven = (x) => x % 2 === 0;
// expect the value to pass a custom predicate function
// the second argument to "sinon.match(predicate, message)" is
// shown if the predicate does not pass and assertion fails
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
/**
* Returns a function that checks if a given number is larger than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isGreaterThan = (limit) => (x) => x > limit
const isGreaterThan = (limit) => (x) => x > limit;
/**
* Returns a function that checks if a given number is less than the limit
* @param {number} limit
* @returns {(x: number) => boolean}
*/
const isLessThan = (limit) => (x) => x < limit
const isLessThan = (limit) => (x) => x < limit;
// you can combine several matchers using "and", "or"
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
)
Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
);
expect(spy).to.be.calledWith(
Cypress.sinon.match.number,
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
)
Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
);
// matchers can be used from BDD assertions
cy.get('@add').should('have.been.calledWith',
Cypress.sinon.match.number, Cypress.sinon.match(3))
cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
// you can alias matchers for shorter test code
const {match: M} = Cypress.sinon
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" />
context('Traversal', () => {
context("Traversal", () => {
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
cy.get('.traversal-breadcrumb')
.children('.active')
.should('contain', 'Data')
})
cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
});
it('.closest() - get closest ancestor DOM element', () => {
it(".closest() - get closest ancestor DOM element", () => {
// https://on.cypress.io/closest
cy.get('.traversal-badge')
.closest('ul')
.should('have.class', 'list-group')
})
cy.get(".traversal-badge").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
cy.get('.traversal-list>li')
.eq(1).should('contain', 'siamese')
})
cy.get(".traversal-list>li").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
cy.get('.traversal-nav>li')
.filter('.active').should('contain', 'About')
})
cy.get(".traversal-nav>li").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
cy.get('.traversal-pagination')
.find('li').find('a')
.should('have.length', 7)
})
cy.get(".traversal-pagination").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
cy.get('.traversal-table td')
.first().should('contain', '1')
})
cy.get(".traversal-table td").first().should("contain", "1");
});
it('.last() - get last DOM element', () => {
it(".last() - get last DOM element", () => {
// https://on.cypress.io/last
cy.get('.traversal-buttons .btn')
.last().should('contain', 'Submit')
})
cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
});
it('.next() - get next sibling DOM element', () => {
it(".next() - get next sibling DOM element", () => {
// https://on.cypress.io/next
cy.get('.traversal-ul')
.contains('apples').next().should('contain', 'oranges')
})
cy.get(".traversal-ul").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
cy.get('.traversal-next-all')
.contains('oranges')
.nextAll().should('have.length', 3)
})
cy.get(".traversal-next-all").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
cy.get('#veggies')
.nextUntil('#nuts').should('have.length', 3)
})
cy.get("#veggies").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
cy.get('.traversal-disabled .btn')
.not('[disabled]').should('not.contain', 'Disabled')
})
cy.get(".traversal-disabled .btn").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
cy.get('.traversal-mark')
.parent().should('contain', 'Morbi leo risus')
})
cy.get(".traversal-mark").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
cy.get('.traversal-cite')
.parents().should('match', 'blockquote')
})
cy.get(".traversal-cite").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
cy.get('.clothes-nav')
.find('.active')
.parentsUntil('.clothes-nav')
.should('have.length', 2)
})
cy.get(".clothes-nav").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
cy.get('.birds').find('.active')
.prev().should('contain', 'Lorikeets')
})
cy.get(".birds").find(".active").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
cy.get('.fruits-list').find('.third')
.prevAll().should('have.length', 2)
})
cy.get(".fruits-list").find(".third").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
cy.get('.foods-list').find('#nuts')
.prevUntil('#veggies').should('have.length', 3)
})
cy.get(".foods-list").find("#nuts").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
cy.get('.traversal-pills .active')
.siblings().should('have.length', 2)
})
})
cy.get(".traversal-pills .active").siblings().should("have.length", 2);
});
});

View File

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

View File

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

View File

@@ -1,31 +1,31 @@
/// <reference types="cypress" />
context('Waiting', () => {
context("Waiting", () => {
beforeEach(() => {
cy.visit('https://example.cypress.io/commands/waiting')
})
cy.visit("https://example.cypress.io/commands/waiting");
});
// BE CAREFUL of adding unnecessary wait times.
// https://on.cypress.io/best-practices#Unnecessary-Waiting
// https://on.cypress.io/wait
it('cy.wait() - wait for a specific amount of time', () => {
cy.get('.wait-input1').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input2').type('Wait 1000ms after typing')
cy.wait(1000)
cy.get('.wait-input3').type('Wait 1000ms after typing')
cy.wait(1000)
})
it("cy.wait() - wait for a specific amount of time", () => {
cy.get(".wait-input1").type("Wait 1000ms after typing");
cy.wait(1000);
cy.get(".wait-input2").type("Wait 1000ms after typing");
cy.wait(1000);
cy.get(".wait-input3").type("Wait 1000ms after typing");
cy.wait(1000);
});
it('cy.wait() - wait for a specific route', () => {
it("cy.wait() - wait for a specific route", () => {
// 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
// the button is clicked in scripts.js
cy.get('.network-btn').click()
cy.get(".network-btn").click();
// 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" />
context('Window', () => {
context("Window", () => {
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
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
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
cy.title().should('include', 'Kitchen Sink')
})
})
cy.title().should("include", "Kitchen Sink");
});
});

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,17 +5,13 @@ import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsAssignmentContainer({
jobLineId,
hours,
refetch,
}) {
export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null,
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
@@ -23,14 +19,14 @@ export default function AllocationsAssignmentContainer({
insertAllocation({ variables: { alloc: { ...assignment } } })
.then((r) => {
notification["success"]({
message: t("allocations.successes.save"),
message: t("allocations.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch((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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
export default connect(
@@ -18,7 +18,7 @@ export default connect(
handleAssignment,
assignment,
setAssignment,
visibilityState,
visibilityState
}) {
const { t } = useTranslation();
@@ -36,9 +36,7 @@ export default connect(
placeholder="Select a person"
optionFilterProp="children"
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
@@ -47,11 +45,7 @@ export default connect(
))}
</Select>
<Button
type="primary"
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign
</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 { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({
jobLines,
refetch,
}) {
export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null,
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
@@ -21,14 +18,14 @@ export default function AllocationsBulkAssignmentContainer({
acc.push({
joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid,
employeeid: assignment.employeeid
});
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
notification["success"]({
message: t("employees.successes.save"),
message: t("employees.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,21 +25,16 @@ import BillDetailEditReturn from "./bill-detail-edit-return.component";
import { PageHeader } from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
@@ -55,7 +50,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
// ... rest of the code remains the same
@@ -64,8 +59,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0
)
setOpen(true);
else {
@@ -81,7 +75,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
const updates = [];
updates.push(
update_bill({
variables: {billId: search.billid, bill: bill},
variables: { billId: search.billid, bill: bill }
})
);
@@ -115,9 +109,9 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
joblineid: il.joblineid === "noline" ? null : il.joblineid
}
}
})
);
} else {
@@ -130,10 +124,10 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
...il,
deductedfromlbr: deductedfromlbr,
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,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
type: "billupdated"
});
await refetch();
@@ -166,10 +160,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
title={data && `${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`}
extra={
<Space>
<BillDetailEditReturn data={data} />
@@ -196,12 +187,7 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
<BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? (
@@ -235,14 +221,13 @@ const transformData = (data) => {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const { t } = useTranslation();
@@ -59,11 +48,11 @@ export function BillDetailEditReturn({
quantity: i.quantity,
joblineid: i.joblineid,
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;
@@ -83,11 +72,7 @@ export function BillDetailEditReturn({
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
@@ -98,12 +83,10 @@ export function BillDetailEditReturn({
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
billlines: form.getFieldsValue().billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
selected: e.target.checked
}))
});
}}
/>
@@ -173,11 +156,7 @@ export function BillDetailEditReturn({
</Form>
</Modal>
<Button
disabled={
data.bills_by_pk.is_credit_memo ||
data.bills_by_pk.isinhouse ||
disabled
}
disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
onClick={() => {
setOpen(true);
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import {createStructuredSelector} from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
@@ -20,18 +20,14 @@ export function PartsReceiveModalComponent({bodyshop, form}) {
name="table"
rules={[
{
required: true,
required: true
//message: t("general.validation.required"),
},
}
]}
>
<Input.TextArea rows={8} />
</Form.Item>
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</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) => {
logImEXEvent("job_ca_bc_pvrt_calculate");
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 }]);
setVisibility(false);
@@ -36,12 +36,7 @@ export default function CABCpvrtCalculator({disabled, form}) {
);
return (
<Popover
destroyTooltipOnHide
content={popContent}
open={visibility}
disabled={disabled}
>
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled />
</Button>

View File

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

View File

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

View File

@@ -20,19 +20,16 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
try {
const r = await axios.post("/notifications/subscribe", {
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",
imexshopid: bodyshop.imexshopid,
imexshopid: bodyshop.imexshopid
});
console.log("FCM Topic Subscription", r.data);
} catch (error) {
console.log(
"Error attempting to subscribe to messaging topic: ",
error
);
console.log("Error attempting to subscribe to messaging topic: ", error);
notification.open({
key: 'fcm',
key: "fcm",
type: "warning",
message: t("general.errors.fcm"),
btn: (
@@ -57,7 +54,7 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
{t("general.labels.help")}
</Button>
</Space>
),
)
});
}
}
@@ -70,7 +67,7 @@ export function ChatAffixContainer({bodyshop, chatVisible}) {
function handleMessage(payload) {
FcmHandler({
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({
variables: { id: conversation.id, archived: !conversation.archived },
refetchQueries: ["CONVERSATION_LIST_QUERY"],
refetchQueries: ["CONVERSATION_LIST_QUERY"]
});
setLoading(false);
@@ -21,9 +21,7 @@ export default function ChatArchiveButton({conversation}) {
return (
<Button onClick={handleToggleArchive} loading={loading} type="primary">
{conversation.archived
? t("messaging.labels.unarchive")
: t("messaging.labels.archive")}
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
</Button>
);
}

View File

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

View File

@@ -15,7 +15,7 @@ export default function ChatConversationTitleTags({jobConversations}) {
removeJobConversation({
variables: {
conversationId: convId,
jobId: jobId,
jobId: jobId
},
update(cache) {
cache.modify({
@@ -23,14 +23,14 @@ export default function ChatConversationTitleTags({jobConversations}) {
fields: {
job_conversations(ex) {
return ex.filter((e) => e.jobid !== jobId);
},
},
}
}
});
},
}
});
logImEXEvent("messaging_remove_job_tag", {
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 }) {
return (
<Space wrap>
<PhoneNumberFormatter>
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags
jobConversations={
(conversation && conversation.job_conversations) || []
}
/>
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
<ChatTagRoContainer conversation={conversation || []} />
<ChatArchiveButton conversation={conversation} />
</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 "./chat-conversation.styles.scss";
export default function ChatConversationComponent({
subState,
conversation,
messages,
handleMarkConversationAsRead,
}) {
export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
const [loading, error] = subState;
if (loading) return <LoadingSkeleton />;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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