Reformat all project files to use the prettier config file.
This commit is contained in:
@@ -1,16 +1,18 @@
|
|||||||
exports.default = {
|
const config = {
|
||||||
printWidth: 120,
|
printWidth: 120,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
trailingComma: 'es5',
|
trailingComma: "none",
|
||||||
semi: true,
|
semi: true,
|
||||||
singleQuote: false,
|
singleQuote: false,
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
arrowParens: 'always',
|
arrowParens: "always",
|
||||||
jsxSingleQuote: false,
|
jsxSingleQuote: false,
|
||||||
bracketSameLine: false,
|
bracketSameLine: false,
|
||||||
endOfLine: 'lf',
|
endOfLine: "lf",
|
||||||
importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'],
|
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||||
importOrderSeparation: true,
|
importOrderSeparation: true,
|
||||||
importOrderSortSpecifiers: true,
|
importOrderSortSpecifiers: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|||||||
@@ -567,4 +567,4 @@
|
|||||||
"description": "Exempt"
|
"description": "Exempt"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
name: "IO Test API",
|
name: "IO Test API",
|
||||||
cwd: "./io",
|
cwd: "./io",
|
||||||
script: "./server.js",
|
script: "./server.js",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "test",
|
NODE_ENV: "test"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Bitbucket Webhook",
|
name: "Bitbucket Webhook",
|
||||||
script: "./webhook/index.js",
|
script: "./webhook/index.js",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production"
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// craco.config.js
|
// craco.config.js
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
const {convertLegacyToken} = require('@ant-design/compatible/lib');
|
const { convertLegacyToken } = require("@ant-design/compatible/lib");
|
||||||
const {theme} = require('antd/lib');
|
const { theme } = require("antd/lib");
|
||||||
|
|
||||||
const {defaultAlgorithm, defaultSeed} = theme;
|
const { defaultAlgorithm, defaultSeed } = theme;
|
||||||
|
|
||||||
const mapToken = defaultAlgorithm(defaultSeed);
|
const mapToken = defaultAlgorithm(defaultSeed);
|
||||||
const v4Token = convertLegacyToken(mapToken);
|
const v4Token = convertLegacyToken(mapToken);
|
||||||
@@ -12,43 +12,42 @@ const v4Token = convertLegacyToken(mapToken);
|
|||||||
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
{
|
||||||
{
|
plugin: CracoLessPlugin,
|
||||||
plugin: CracoLessPlugin,
|
options: {
|
||||||
options: {
|
lessLoaderOptions: {
|
||||||
lessLoaderOptions: {
|
lessOptions: {
|
||||||
lessOptions: {
|
modifyVars: { ...v4Token },
|
||||||
modifyVars: {...v4Token},
|
javascriptEnabled: true
|
||||||
javascriptEnabled: true,
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
webpack: {
|
||||||
|
configure: (webpackConfig) => {
|
||||||
|
return {
|
||||||
|
...webpackConfig,
|
||||||
|
// Required for Dev Server
|
||||||
|
devServer: {
|
||||||
|
...webpackConfig.devServer,
|
||||||
|
allowedHosts: "all"
|
||||||
},
|
},
|
||||||
],
|
optimization: {
|
||||||
webpack: {
|
...webpackConfig.optimization,
|
||||||
configure: (webpackConfig) => {
|
// Workaround for CircleCI bug caused by the number of CPUs shown
|
||||||
return {
|
// https://github.com/facebook/create-react-app/issues/8320
|
||||||
...webpackConfig,
|
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
||||||
// Required for Dev Server
|
if (item instanceof TerserPlugin) {
|
||||||
devServer: {
|
item.options.parallel = 2;
|
||||||
...webpackConfig.devServer,
|
}
|
||||||
allowedHosts: 'all',
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
...webpackConfig.optimization,
|
|
||||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
|
||||||
// https://github.com/facebook/create-react-app/issues/8320
|
|
||||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
|
||||||
if (item instanceof TerserPlugin) {
|
|
||||||
item.options.parallel = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
devtool: "source-map",
|
devtool: "source-map"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
const {defineConfig} = require('cypress')
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
experimentalStudio: true,
|
experimentalStudio: true,
|
||||||
env: {
|
env: {
|
||||||
FIREBASE_USERNAME: 'cypress@imex.test',
|
FIREBASE_USERNAME: "cypress@imex.test",
|
||||||
FIREBASE_PASSWORD: 'cypress',
|
FIREBASE_PASSWORD: "cypress"
|
||||||
|
},
|
||||||
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require("./cypress/plugins/index.js")(on, config);
|
||||||
},
|
},
|
||||||
e2e: {
|
baseUrl: "http://localhost:3000"
|
||||||
// We've imported your old cypress plugins here.
|
}
|
||||||
// You may want to clean this up later by importing these.
|
});
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
return require('./cypress/plugins/index.js')(on, config)
|
|
||||||
},
|
|
||||||
baseUrl: 'http://localhost:3000',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
/// <reference types="Cypress" />
|
/// <reference types="Cypress" />
|
||||||
const {FIREBASE_USERNAME, FIREBASE_PASSWORcD} = Cypress.env();
|
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
||||||
describe("Renders the General Page", () => {
|
describe("Renders the General Page", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit("/");
|
cy.visit("/");
|
||||||
});
|
});
|
||||||
it("Renders Correctly", () => {
|
it("Renders Correctly", () => {});
|
||||||
});
|
it("Has the Slogan", () => {
|
||||||
it("Has the Slogan", () => {
|
cy.findByText("A whole x22new kind of shop management system.").should("exist");
|
||||||
cy.findByText("A whole x22new kind of shop management system.").should(
|
/* ==== Generated with Cypress Studio ==== */
|
||||||
"exist"
|
cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
|
||||||
);
|
cy.get("#email").clear();
|
||||||
/* ==== Generated with Cypress Studio ==== */
|
cy.get("#email").type("patrick@imex.dev");
|
||||||
cy.get(
|
cy.get("#password").clear();
|
||||||
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
|
cy.get("#password").type("patrick123{enter}");
|
||||||
).click();
|
cy.get(".ant-form > .ant-btn").click();
|
||||||
cy.get("#email").clear();
|
/* ==== End Cypress Studio ==== */
|
||||||
cy.get("#email").type("patrick@imex.dev");
|
});
|
||||||
cy.get("#password").clear();
|
|
||||||
cy.get("#password").type("patrick123{enter}");
|
|
||||||
cy.get(".ant-form > .ant-btn").click();
|
|
||||||
/* ==== End Cypress Studio ==== */
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,133 +11,114 @@
|
|||||||
// please read our getting started guide:
|
// please read our getting started guide:
|
||||||
// https://on.cypress.io/introduction-to-cypress
|
// https://on.cypress.io/introduction-to-cypress
|
||||||
|
|
||||||
describe('example to-do app', () => {
|
describe("example to-do app", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Cypress starts out with a blank slate for each test
|
||||||
|
// so we must tell it to visit our website with the `cy.visit()` command.
|
||||||
|
// Since we want to visit the same URL at the start of all our tests,
|
||||||
|
// we include it in our beforeEach function so that it runs before each test
|
||||||
|
cy.visit("https://example.cypress.io/todo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays two todo items by default", () => {
|
||||||
|
// We use the `cy.get()` command to get all elements that match the selector.
|
||||||
|
// Then, we use `should` to assert that there are two matched items,
|
||||||
|
// which are the two default items.
|
||||||
|
cy.get(".todo-list li").should("have.length", 2);
|
||||||
|
|
||||||
|
// We can go even further and check that the default todos each contain
|
||||||
|
// the correct text. We use the `first` and `last` functions
|
||||||
|
// to get just the first and last matched elements individually,
|
||||||
|
// and then perform an assertion with `should`.
|
||||||
|
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
|
||||||
|
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can add new todo items", () => {
|
||||||
|
// We'll store our item text in a variable so we can reuse it
|
||||||
|
const newItem = "Feed the cat";
|
||||||
|
|
||||||
|
// Let's get the input element and use the `type` command to
|
||||||
|
// input our new list item. After typing the content of our item,
|
||||||
|
// we need to type the enter key as well in order to submit the input.
|
||||||
|
// This input has a data-test attribute so we'll use that to select the
|
||||||
|
// element in accordance with best practices:
|
||||||
|
// https://on.cypress.io/selecting-elements
|
||||||
|
cy.get("[data-test=new-todo]").type(`${newItem}{enter}`);
|
||||||
|
|
||||||
|
// Now that we've typed our new item, let's check that it actually was added to the list.
|
||||||
|
// Since it's the newest item, it should exist as the last element in the list.
|
||||||
|
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
||||||
|
// Since assertions yield the element that was asserted on,
|
||||||
|
// we can chain both of these assertions together into a single statement.
|
||||||
|
cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can check off an item as completed", () => {
|
||||||
|
// In addition to using the `get` command to get an element by selector,
|
||||||
|
// we can also use the `contains` command to get an element by its contents.
|
||||||
|
// However, this will yield the <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();
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
});
|
||||||
|
|
||||||
|
context("with a checked task", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Cypress starts out with a blank slate for each test
|
// We'll take the command we used above to check off an element
|
||||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
// Since we want to perform multiple tests that start with checking
|
||||||
// Since we want to visit the same URL at the start of all our tests,
|
// one element, we put it in the beforeEach hook
|
||||||
// we include it in our beforeEach function so that it runs before each test
|
// so that it runs at the start of every test.
|
||||||
cy.visit('https://example.cypress.io/todo')
|
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
|
||||||
})
|
});
|
||||||
|
|
||||||
it('displays two todo items by default', () => {
|
it("can filter for uncompleted tasks", () => {
|
||||||
// We use the `cy.get()` command to get all elements that match the selector.
|
// We'll click on the "active" button in order to
|
||||||
// Then, we use `should` to assert that there are two matched items,
|
// display only incomplete items
|
||||||
// which are the two default items.
|
cy.contains("Active").click();
|
||||||
cy.get('.todo-list li').should('have.length', 2)
|
|
||||||
|
|
||||||
// We can go even further and check that the default todos each contain
|
// After filtering, we can assert that there is only the one
|
||||||
// the correct text. We use the `first` and `last` functions
|
// incomplete item in the list.
|
||||||
// to get just the first and last matched elements individually,
|
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog");
|
||||||
// and then perform an assertion with `should`.
|
|
||||||
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
|
|
||||||
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can add new todo items', () => {
|
// For good measure, let's also assert that the task we checked off
|
||||||
// We'll store our item text in a variable so we can reuse it
|
// does not exist on the page.
|
||||||
const newItem = 'Feed the cat'
|
cy.contains("Pay electric bill").should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
// Let's get the input element and use the `type` command to
|
it("can filter for completed tasks", () => {
|
||||||
// input our new list item. After typing the content of our item,
|
// We can perform similar steps as the test above to ensure
|
||||||
// we need to type the enter key as well in order to submit the input.
|
// that only completed tasks are shown
|
||||||
// This input has a data-test attribute so we'll use that to select the
|
cy.contains("Completed").click();
|
||||||
// element in accordance with best practices:
|
|
||||||
// https://on.cypress.io/selecting-elements
|
|
||||||
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
|
|
||||||
|
|
||||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill");
|
||||||
// Since it's the newest item, it should exist as the last element in the list.
|
|
||||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
|
||||||
// Since assertions yield the element that was asserted on,
|
|
||||||
// we can chain both of these assertions together into a single statement.
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 3)
|
|
||||||
.last()
|
|
||||||
.should('have.text', newItem)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can check off an item as completed', () => {
|
cy.contains("Walk the dog").should("not.exist");
|
||||||
// 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()
|
|
||||||
|
|
||||||
// Now that we've checked the button, we can go ahead and make sure
|
it("can delete all completed tasks", () => {
|
||||||
// that the list element is now marked as completed.
|
// First, let's click the "Clear completed" button
|
||||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
// `contains` is actually serving two purposes here.
|
||||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
// First, it's ensuring that the button exists within the dom.
|
||||||
// Once we get that element, we can assert that it has the completed class.
|
// This button only appears when at least one task is checked
|
||||||
cy.contains('Pay electric bill')
|
// so this command is implicitly verifying that it does exist.
|
||||||
.parents('li')
|
// Second, it selects the button so we can click it.
|
||||||
.should('have.class', 'completed')
|
cy.contains("Clear completed").click();
|
||||||
})
|
|
||||||
|
|
||||||
context('with a checked task', () => {
|
// Then we can make sure that there is only one element
|
||||||
beforeEach(() => {
|
// in the list and our element does not exist
|
||||||
// We'll take the command we used above to check off an element
|
cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill");
|
||||||
// Since we want to perform multiple tests that start with checking
|
|
||||||
// one element, we put it in the beforeEach hook
|
|
||||||
// so that it runs at the start of every test.
|
|
||||||
cy.contains('Pay electric bill')
|
|
||||||
.parent()
|
|
||||||
.find('input[type=checkbox]')
|
|
||||||
.check()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can filter for uncompleted tasks', () => {
|
// Finally, make sure that the clear button no longer exists.
|
||||||
// We'll click on the "active" button in order to
|
cy.contains("Clear completed").should("not.exist");
|
||||||
// display only incomplete items
|
});
|
||||||
cy.contains('Active').click()
|
});
|
||||||
|
});
|
||||||
// After filtering, we can assert that there is only the one
|
|
||||||
// incomplete item in the list.
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.first()
|
|
||||||
.should('have.text', 'Walk the dog')
|
|
||||||
|
|
||||||
// For good measure, let's also assert that the task we checked off
|
|
||||||
// does not exist on the page.
|
|
||||||
cy.contains('Pay electric bill').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can filter for completed tasks', () => {
|
|
||||||
// We can perform similar steps as the test above to ensure
|
|
||||||
// that only completed tasks are shown
|
|
||||||
cy.contains('Completed').click()
|
|
||||||
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.first()
|
|
||||||
.should('have.text', 'Pay electric bill')
|
|
||||||
|
|
||||||
cy.contains('Walk the dog').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can delete all completed tasks', () => {
|
|
||||||
// First, let's click the "Clear completed" button
|
|
||||||
// `contains` is actually serving two purposes here.
|
|
||||||
// First, it's ensuring that the button exists within the dom.
|
|
||||||
// This button only appears when at least one task is checked
|
|
||||||
// so this command is implicitly verifying that it does exist.
|
|
||||||
// Second, it selects the button so we can click it.
|
|
||||||
cy.contains('Clear completed').click()
|
|
||||||
|
|
||||||
// Then we can make sure that there is only one element
|
|
||||||
// in the list and our element does not exist
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.should('not.have.text', 'Pay electric bill')
|
|
||||||
|
|
||||||
// Finally, make sure that the clear button no longer exists.
|
|
||||||
cy.contains('Clear completed').should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,299 +1,284 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Actions', () => {
|
context("Actions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/actions')
|
cy.visit("https://example.cypress.io/commands/actions");
|
||||||
})
|
});
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
|
||||||
it('.type() - type into a DOM element', () => {
|
it(".type() - type into a DOM element", () => {
|
||||||
// https://on.cypress.io/type
|
// https://on.cypress.io/type
|
||||||
cy.get('.action-email')
|
cy.get(".action-email")
|
||||||
.type('fake@email.com').should('have.value', 'fake@email.com')
|
.type("fake@email.com")
|
||||||
|
.should("have.value", "fake@email.com")
|
||||||
|
|
||||||
// .type() with special character sequences
|
// .type() with special character sequences
|
||||||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
|
.type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
|
||||||
.type('{del}{selectall}{backspace}')
|
.type("{del}{selectall}{backspace}")
|
||||||
|
|
||||||
// .type() with key modifiers
|
// .type() with key modifiers
|
||||||
.type('{alt}{option}') //these are equivalent
|
.type("{alt}{option}") //these are equivalent
|
||||||
.type('{ctrl}{control}') //these are equivalent
|
.type("{ctrl}{control}") //these are equivalent
|
||||||
.type('{meta}{command}{cmd}') //these are equivalent
|
.type("{meta}{command}{cmd}") //these are equivalent
|
||||||
.type('{shift}')
|
.type("{shift}")
|
||||||
|
|
||||||
// Delay each keypress by 0.1 sec
|
// Delay each keypress by 0.1 sec
|
||||||
.type('slow.typing@email.com', {delay: 100})
|
.type("slow.typing@email.com", { delay: 100 })
|
||||||
.should('have.value', 'slow.typing@email.com')
|
.should("have.value", "slow.typing@email.com");
|
||||||
|
|
||||||
cy.get('.action-disabled')
|
cy.get(".action-disabled")
|
||||||
// Ignore error checking prior to type
|
// Ignore error checking prior to type
|
||||||
// like whether the input is visible or disabled
|
// like whether the input is visible or disabled
|
||||||
.type('disabled error checking', {force: true})
|
.type("disabled error checking", { force: true })
|
||||||
.should('have.value', 'disabled error checking')
|
.should("have.value", "disabled error checking");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.focus() - focus on a DOM element', () => {
|
it(".focus() - focus on a DOM element", () => {
|
||||||
// https://on.cypress.io/focus
|
// https://on.cypress.io/focus
|
||||||
cy.get('.action-focus').focus()
|
cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;");
|
||||||
.should('have.class', 'focus')
|
});
|
||||||
.prev().should('have.attr', 'style', 'color: orange;')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.blur() - blur off a DOM element', () => {
|
it(".blur() - blur off a DOM element", () => {
|
||||||
// https://on.cypress.io/blur
|
// https://on.cypress.io/blur
|
||||||
cy.get('.action-blur').type('About to blur').blur()
|
cy.get(".action-blur")
|
||||||
.should('have.class', 'error')
|
.type("About to blur")
|
||||||
.prev().should('have.attr', 'style', 'color: red;')
|
.blur()
|
||||||
})
|
.should("have.class", "error")
|
||||||
|
.prev()
|
||||||
|
.should("have.attr", "style", "color: red;");
|
||||||
|
});
|
||||||
|
|
||||||
it('.clear() - clears an input or textarea element', () => {
|
it(".clear() - clears an input or textarea element", () => {
|
||||||
// https://on.cypress.io/clear
|
// https://on.cypress.io/clear
|
||||||
cy.get('.action-clear').type('Clear this text')
|
cy.get(".action-clear")
|
||||||
.should('have.value', 'Clear this text')
|
.type("Clear this text")
|
||||||
.clear()
|
.should("have.value", "Clear this text")
|
||||||
.should('have.value', '')
|
.clear()
|
||||||
})
|
.should("have.value", "");
|
||||||
|
});
|
||||||
|
|
||||||
it('.submit() - submit a form', () => {
|
it(".submit() - submit a form", () => {
|
||||||
// https://on.cypress.io/submit
|
// https://on.cypress.io/submit
|
||||||
cy.get('.action-form')
|
cy.get(".action-form").find('[type="text"]').type("HALFOFF");
|
||||||
.find('[type="text"]').type('HALFOFF')
|
|
||||||
|
|
||||||
cy.get('.action-form').submit()
|
cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
|
||||||
.next().should('contain', 'Your form has been submitted!')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.click() - click on a DOM element', () => {
|
it(".click() - click on a DOM element", () => {
|
||||||
// https://on.cypress.io/click
|
// https://on.cypress.io/click
|
||||||
cy.get('.action-btn').click()
|
cy.get(".action-btn").click();
|
||||||
|
|
||||||
// You can click on 9 specific positions of an element:
|
// You can click on 9 specific positions of an element:
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// | topLeft top topRight |
|
// | topLeft top topRight |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | left center right |
|
// | left center right |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | bottomLeft bottom bottomRight |
|
// | bottomLeft bottom bottomRight |
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
||||||
// clicking in the center of the element is the default
|
// clicking in the center of the element is the default
|
||||||
cy.get('#action-canvas').click()
|
cy.get("#action-canvas").click();
|
||||||
|
|
||||||
cy.get('#action-canvas').click('topLeft')
|
cy.get("#action-canvas").click("topLeft");
|
||||||
cy.get('#action-canvas').click('top')
|
cy.get("#action-canvas").click("top");
|
||||||
cy.get('#action-canvas').click('topRight')
|
cy.get("#action-canvas").click("topRight");
|
||||||
cy.get('#action-canvas').click('left')
|
cy.get("#action-canvas").click("left");
|
||||||
cy.get('#action-canvas').click('right')
|
cy.get("#action-canvas").click("right");
|
||||||
cy.get('#action-canvas').click('bottomLeft')
|
cy.get("#action-canvas").click("bottomLeft");
|
||||||
cy.get('#action-canvas').click('bottom')
|
cy.get("#action-canvas").click("bottom");
|
||||||
cy.get('#action-canvas').click('bottomRight')
|
cy.get("#action-canvas").click("bottomRight");
|
||||||
|
|
||||||
// .click() accepts an x and y coordinate
|
// .click() accepts an x and y coordinate
|
||||||
// that controls where the click occurs :)
|
// that controls where the click occurs :)
|
||||||
|
|
||||||
cy.get('#action-canvas')
|
cy.get("#action-canvas")
|
||||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
.click(80, 75) // click 80px on x coord and 75px on y coord
|
||||||
.click(170, 75)
|
.click(170, 75)
|
||||||
.click(80, 165)
|
.click(80, 165)
|
||||||
.click(100, 185)
|
.click(100, 185)
|
||||||
.click(125, 190)
|
.click(125, 190)
|
||||||
.click(150, 185)
|
.click(150, 185)
|
||||||
.click(170, 165)
|
.click(170, 165);
|
||||||
|
|
||||||
// click multiple elements by passing multiple: true
|
// click multiple elements by passing multiple: true
|
||||||
cy.get('.action-labels>.label').click({multiple: true})
|
cy.get(".action-labels>.label").click({ multiple: true });
|
||||||
|
|
||||||
// Ignore error checking prior to clicking
|
// Ignore error checking prior to clicking
|
||||||
cy.get('.action-opacity>.btn').click({force: true})
|
cy.get(".action-opacity>.btn").click({ force: true });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.dblclick() - double click on a DOM element', () => {
|
it(".dblclick() - double click on a DOM element", () => {
|
||||||
// https://on.cypress.io/dblclick
|
// https://on.cypress.io/dblclick
|
||||||
|
|
||||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
||||||
// that hides the div and shows an input on double click
|
// that hides the div and shows an input on double click
|
||||||
cy.get('.action-div').dblclick().should('not.be.visible')
|
cy.get(".action-div").dblclick().should("not.be.visible");
|
||||||
cy.get('.action-input-hidden').should('be.visible')
|
cy.get(".action-input-hidden").should("be.visible");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.rightclick() - right click on a DOM element', () => {
|
it(".rightclick() - right click on a DOM element", () => {
|
||||||
// https://on.cypress.io/rightclick
|
// https://on.cypress.io/rightclick
|
||||||
|
|
||||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
||||||
// that hides the div and shows an input on right click
|
// that hides the div and shows an input on right click
|
||||||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
|
cy.get(".rightclick-action-div").rightclick().should("not.be.visible");
|
||||||
cy.get('.rightclick-action-input-hidden').should('be.visible')
|
cy.get(".rightclick-action-input-hidden").should("be.visible");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.check() - check a checkbox or radio element', () => {
|
it(".check() - check a checkbox or radio element", () => {
|
||||||
// https://on.cypress.io/check
|
// https://on.cypress.io/check
|
||||||
|
|
||||||
// By default, .check() will check all
|
// By default, .check() will check all
|
||||||
// matching checkbox or radio elements in succession, one after another
|
// matching checkbox or radio elements in succession, one after another
|
||||||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
|
cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
|
||||||
.check().should('be.checked')
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').not('[disabled]')
|
cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
|
||||||
.check().should('be.checked')
|
|
||||||
|
|
||||||
// .check() accepts a value argument
|
// .check() accepts a value argument
|
||||||
cy.get('.action-radios [type="radio"]')
|
cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
|
||||||
.check('radio1').should('be.checked')
|
|
||||||
|
|
||||||
// .check() accepts an array of values
|
// .check() accepts an array of values
|
||||||
cy.get('.action-multiple-checkboxes [type="checkbox"]')
|
cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
|
||||||
.check(['checkbox1', 'checkbox2']).should('be.checked')
|
|
||||||
|
|
||||||
// Ignore error checking prior to checking
|
// Ignore error checking prior to checking
|
||||||
cy.get('.action-checkboxes [disabled]')
|
cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
|
||||||
.check({force: true}).should('be.checked')
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]')
|
cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
|
||||||
.check('radio3', {force: true}).should('be.checked')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.uncheck() - uncheck a checkbox element', () => {
|
it(".uncheck() - uncheck a checkbox element", () => {
|
||||||
// https://on.cypress.io/uncheck
|
// https://on.cypress.io/uncheck
|
||||||
|
|
||||||
// By default, .uncheck() will uncheck all matching
|
// By default, .uncheck() will uncheck all matching
|
||||||
// checkbox elements in succession, one after another
|
// checkbox elements in succession, one after another
|
||||||
cy.get('.action-check [type="checkbox"]')
|
cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
|
||||||
.not('[disabled]')
|
|
||||||
.uncheck().should('not.be.checked')
|
|
||||||
|
|
||||||
// .uncheck() accepts a value argument
|
// .uncheck() accepts a value argument
|
||||||
cy.get('.action-check [type="checkbox"]')
|
cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
|
||||||
.check('checkbox1')
|
|
||||||
.uncheck('checkbox1').should('not.be.checked')
|
|
||||||
|
|
||||||
// .uncheck() accepts an array of values
|
// .uncheck() accepts an array of values
|
||||||
cy.get('.action-check [type="checkbox"]')
|
cy.get('.action-check [type="checkbox"]')
|
||||||
.check(['checkbox1', 'checkbox3'])
|
.check(["checkbox1", "checkbox3"])
|
||||||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
|
.uncheck(["checkbox1", "checkbox3"])
|
||||||
|
.should("not.be.checked");
|
||||||
|
|
||||||
// Ignore error checking prior to unchecking
|
// Ignore error checking prior to unchecking
|
||||||
cy.get('.action-check [disabled]')
|
cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked");
|
||||||
.uncheck({force: true}).should('not.be.checked')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.select() - select an option in a <select> element', () => {
|
it(".select() - select an option in a <select> element", () => {
|
||||||
// https://on.cypress.io/select
|
// https://on.cypress.io/select
|
||||||
|
|
||||||
// at first, no option should be selected
|
// at first, no option should be selected
|
||||||
cy.get('.action-select')
|
cy.get(".action-select").should("have.value", "--Select a fruit--");
|
||||||
.should('have.value', '--Select a fruit--')
|
|
||||||
|
|
||||||
// Select option(s) with matching text content
|
// Select option(s) with matching text content
|
||||||
cy.get('.action-select').select('apples')
|
cy.get(".action-select").select("apples");
|
||||||
// confirm the apples were selected
|
// confirm the apples were selected
|
||||||
// note that each value starts with "fr-" in our HTML
|
// note that each value starts with "fr-" in our HTML
|
||||||
cy.get('.action-select').should('have.value', 'fr-apples')
|
cy.get(".action-select").should("have.value", "fr-apples");
|
||||||
|
|
||||||
cy.get('.action-select-multiple')
|
cy.get(".action-select-multiple")
|
||||||
.select(['apples', 'oranges', 'bananas'])
|
.select(["apples", "oranges", "bananas"])
|
||||||
// when getting multiple values, invoke "val" method first
|
// when getting multiple values, invoke "val" method first
|
||||||
.invoke('val')
|
.invoke("val")
|
||||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
||||||
|
|
||||||
// Select option(s) with matching value
|
// Select option(s) with matching value
|
||||||
cy.get('.action-select').select('fr-bananas')
|
cy.get(".action-select")
|
||||||
// can attach an assertion right away to the element
|
.select("fr-bananas")
|
||||||
.should('have.value', 'fr-bananas')
|
// can attach an assertion right away to the element
|
||||||
|
.should("have.value", "fr-bananas");
|
||||||
|
|
||||||
cy.get('.action-select-multiple')
|
cy.get(".action-select-multiple")
|
||||||
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
|
.select(["fr-apples", "fr-oranges", "fr-bananas"])
|
||||||
.invoke('val')
|
.invoke("val")
|
||||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
||||||
|
|
||||||
// assert the selected values include oranges
|
// assert the selected values include oranges
|
||||||
cy.get('.action-select-multiple')
|
cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges");
|
||||||
.invoke('val').should('include', 'fr-oranges')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.scrollIntoView() - scroll an element into view', () => {
|
it(".scrollIntoView() - scroll an element into view", () => {
|
||||||
// https://on.cypress.io/scrollintoview
|
// https://on.cypress.io/scrollintoview
|
||||||
|
|
||||||
// normally all of these buttons are hidden,
|
// normally all of these buttons are hidden,
|
||||||
// because they're not within
|
// because they're not within
|
||||||
// the viewable area of their parent
|
// the viewable area of their parent
|
||||||
// (we need to scroll to see them)
|
// (we need to scroll to see them)
|
||||||
cy.get('#scroll-horizontal button')
|
cy.get("#scroll-horizontal button").should("not.be.visible");
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// scroll the button into view, as if the user had scrolled
|
// scroll the button into view, as if the user had scrolled
|
||||||
cy.get('#scroll-horizontal button').scrollIntoView()
|
cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
|
||||||
.should('be.visible')
|
|
||||||
|
|
||||||
cy.get('#scroll-vertical button')
|
cy.get("#scroll-vertical button").should("not.be.visible");
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// Cypress handles the scroll direction needed
|
// Cypress handles the scroll direction needed
|
||||||
cy.get('#scroll-vertical button').scrollIntoView()
|
cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
|
||||||
.should('be.visible')
|
|
||||||
|
|
||||||
cy.get('#scroll-both button')
|
cy.get("#scroll-both button").should("not.be.visible");
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// Cypress knows to scroll to the right and down
|
// Cypress knows to scroll to the right and down
|
||||||
cy.get('#scroll-both button').scrollIntoView()
|
cy.get("#scroll-both button").scrollIntoView().should("be.visible");
|
||||||
.should('be.visible')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.trigger() - trigger an event on a DOM element', () => {
|
it(".trigger() - trigger an event on a DOM element", () => {
|
||||||
// https://on.cypress.io/trigger
|
// https://on.cypress.io/trigger
|
||||||
|
|
||||||
// To interact with a range input (slider)
|
// To interact with a range input (slider)
|
||||||
// we need to set its value & trigger the
|
// we need to set its value & trigger the
|
||||||
// event to signal it changed
|
// event to signal it changed
|
||||||
|
|
||||||
// Here, we invoke jQuery's val() method to set
|
// Here, we invoke jQuery's val() method to set
|
||||||
// the value and trigger the 'change' event
|
// the value and trigger the 'change' event
|
||||||
cy.get('.trigger-input-range')
|
cy.get(".trigger-input-range")
|
||||||
.invoke('val', 25)
|
.invoke("val", 25)
|
||||||
.trigger('change')
|
.trigger("change")
|
||||||
.get('input[type=range]').siblings('p')
|
.get("input[type=range]")
|
||||||
.should('have.text', '25')
|
.siblings("p")
|
||||||
})
|
.should("have.text", "25");
|
||||||
|
});
|
||||||
|
|
||||||
it('cy.scrollTo() - scroll the window or element to a position', () => {
|
it("cy.scrollTo() - scroll the window or element to a position", () => {
|
||||||
// https://on.cypress.io/scrollto
|
// https://on.cypress.io/scrollto
|
||||||
|
|
||||||
// You can scroll to 9 specific positions of an element:
|
// You can scroll to 9 specific positions of an element:
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// | topLeft top topRight |
|
// | topLeft top topRight |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | left center right |
|
// | left center right |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | |
|
// | |
|
||||||
// | bottomLeft bottom bottomRight |
|
// | bottomLeft bottom bottomRight |
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
||||||
// if you chain .scrollTo() off of cy, we will
|
// if you chain .scrollTo() off of cy, we will
|
||||||
// scroll the entire window
|
// scroll the entire window
|
||||||
cy.scrollTo('bottom')
|
cy.scrollTo("bottom");
|
||||||
|
|
||||||
cy.get('#scrollable-horizontal').scrollTo('right')
|
cy.get("#scrollable-horizontal").scrollTo("right");
|
||||||
|
|
||||||
// or you can scroll to a specific coordinate:
|
// or you can scroll to a specific coordinate:
|
||||||
// (x axis, y axis) in pixels
|
// (x axis, y axis) in pixels
|
||||||
cy.get('#scrollable-vertical').scrollTo(250, 250)
|
cy.get("#scrollable-vertical").scrollTo(250, 250);
|
||||||
|
|
||||||
// or you can scroll to a specific percentage
|
// or you can scroll to a specific percentage
|
||||||
// of the (width, height) of the element
|
// of the (width, height) of the element
|
||||||
cy.get('#scrollable-both').scrollTo('75%', '25%')
|
cy.get("#scrollable-both").scrollTo("75%", "25%");
|
||||||
|
|
||||||
// control the easing of the scroll (default is 'swing')
|
// control the easing of the scroll (default is 'swing')
|
||||||
cy.get('#scrollable-vertical').scrollTo('center', {easing: 'linear'})
|
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
|
||||||
|
|
||||||
// control the duration of the scroll (in ms)
|
// control the duration of the scroll (in ms)
|
||||||
cy.get('#scrollable-both').scrollTo('center', {duration: 2000})
|
cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,39 +1,35 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Aliasing', () => {
|
context("Aliasing", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/aliasing')
|
cy.visit("https://example.cypress.io/commands/aliasing");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.as() - alias a DOM element for later use', () => {
|
it(".as() - alias a DOM element for later use", () => {
|
||||||
// https://on.cypress.io/as
|
// https://on.cypress.io/as
|
||||||
|
|
||||||
// Alias a DOM element for use later
|
// Alias a DOM element for use later
|
||||||
// We don't have to traverse to the element
|
// We don't have to traverse to the element
|
||||||
// later in our code, we reference it with @
|
// later in our code, we reference it with @
|
||||||
|
|
||||||
cy.get('.as-table').find('tbody>tr')
|
cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
|
||||||
.first().find('td').first()
|
|
||||||
.find('button').as('firstBtn')
|
|
||||||
|
|
||||||
// when we reference the alias, we place an
|
// when we reference the alias, we place an
|
||||||
// @ in front of its name
|
// @ in front of its name
|
||||||
cy.get('@firstBtn').click()
|
cy.get("@firstBtn").click();
|
||||||
|
|
||||||
cy.get('@firstBtn')
|
cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
|
||||||
.should('have.class', 'btn-success')
|
});
|
||||||
.and('contain', 'Changed')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.as() - alias a route for later use', () => {
|
it(".as() - alias a route for later use", () => {
|
||||||
// Alias the route to wait for its response
|
// Alias the route to wait for its response
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
cy.intercept("GET", "**/comments/*").as("getComment");
|
||||||
|
|
||||||
// we have code that gets a comment when
|
// we have code that gets a comment when
|
||||||
// the button is clicked in scripts.js
|
// the button is clicked in scripts.js
|
||||||
cy.get('.network-btn').click()
|
cy.get(".network-btn").click();
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
// https://on.cypress.io/wait
|
||||||
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
|
cy.wait("@getComment").its("response.statusCode").should("eq", 200);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,177 +1,173 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Assertions', () => {
|
context("Assertions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/assertions')
|
cy.visit("https://example.cypress.io/commands/assertions");
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('Implicit Assertions', () => {
|
describe("Implicit Assertions", () => {
|
||||||
it('.should() - make an assertion about the current subject', () => {
|
it(".should() - make an assertion about the current subject", () => {
|
||||||
// https://on.cypress.io/should
|
// https://on.cypress.io/should
|
||||||
cy.get('.assertion-table')
|
cy.get(".assertion-table")
|
||||||
.find('tbody tr:last')
|
.find("tbody tr:last")
|
||||||
.should('have.class', 'success')
|
.should("have.class", "success")
|
||||||
.find('td')
|
.find("td")
|
||||||
.first()
|
.first()
|
||||||
// checking the text of the <td> element in various ways
|
// checking the text of the <td> element in various ways
|
||||||
.should('have.text', 'Column content')
|
.should("have.text", "Column content")
|
||||||
.should('contain', 'Column content')
|
.should("contain", "Column content")
|
||||||
.should('have.html', 'Column content')
|
.should("have.html", "Column content")
|
||||||
// chai-jquery uses "is()" to check if element matches selector
|
// chai-jquery uses "is()" to check if element matches selector
|
||||||
.should('match', 'td')
|
.should("match", "td")
|
||||||
// to match text content against a regular expression
|
// to match text content against a regular expression
|
||||||
// first need to invoke jQuery method text()
|
// first need to invoke jQuery method text()
|
||||||
// and then match using regular expression
|
// and then match using regular expression
|
||||||
.invoke('text')
|
.invoke("text")
|
||||||
.should('match', /column content/i)
|
.should("match", /column content/i);
|
||||||
|
|
||||||
// a better way to check element's text content against a regular expression
|
// a better way to check element's text content against a regular expression
|
||||||
// is to use "cy.contains"
|
// is to use "cy.contains"
|
||||||
// https://on.cypress.io/contains
|
// https://on.cypress.io/contains
|
||||||
cy.get('.assertion-table')
|
cy.get(".assertion-table")
|
||||||
.find('tbody tr:last')
|
.find("tbody tr:last")
|
||||||
// finds first <td> element with text content matching regular expression
|
// finds first <td> element with text content matching regular expression
|
||||||
.contains('td', /column content/i)
|
.contains("td", /column content/i)
|
||||||
.should('be.visible')
|
.should("be.visible");
|
||||||
|
|
||||||
// for more information about asserting element's text
|
// for more information about asserting element's text
|
||||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
||||||
|
});
|
||||||
|
|
||||||
|
it(".and() - chain multiple assertions together", () => {
|
||||||
|
// https://on.cypress.io/and
|
||||||
|
cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Explicit Assertions", () => {
|
||||||
|
// https://on.cypress.io/assertions
|
||||||
|
it("expect - make an assertion about a specified subject", () => {
|
||||||
|
// We can use Chai's BDD style assertions
|
||||||
|
expect(true).to.be.true;
|
||||||
|
const o = { foo: "bar" };
|
||||||
|
|
||||||
|
expect(o).to.equal(o);
|
||||||
|
expect(o).to.deep.equal({ foo: "bar" });
|
||||||
|
// matching text using regular expression
|
||||||
|
expect("FooBar").to.match(/bar$/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pass your own callback function to should()", () => {
|
||||||
|
// Pass a function to should that can have any number
|
||||||
|
// of explicit assertions within it.
|
||||||
|
// The ".should(cb)" function will be retried
|
||||||
|
// automatically until it passes all your explicit assertions or times out.
|
||||||
|
cy.get(".assertions-p")
|
||||||
|
.find("p")
|
||||||
|
.should(($p) => {
|
||||||
|
// https://on.cypress.io/$
|
||||||
|
// return an array of texts from all of the p's
|
||||||
|
// @ts-ignore TS6133 unused variable
|
||||||
|
const texts = $p.map((i, el) => Cypress.$(el).text());
|
||||||
|
|
||||||
|
// jquery map returns jquery object
|
||||||
|
// and .get() convert this to simple array
|
||||||
|
const paragraphs = texts.get();
|
||||||
|
|
||||||
|
// array should have length of 3
|
||||||
|
expect(paragraphs, "has 3 paragraphs").to.have.length(3);
|
||||||
|
|
||||||
|
// use second argument to expect(...) to provide clear
|
||||||
|
// message with each assertion
|
||||||
|
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
|
||||||
|
"Some text from first p",
|
||||||
|
"More text from second p",
|
||||||
|
"And even more text from third p"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds element by class name regex", () => {
|
||||||
|
cy.get(".docs-header")
|
||||||
|
.find("div")
|
||||||
|
// .should(cb) callback function will be retried
|
||||||
|
.should(($div) => {
|
||||||
|
expect($div).to.have.length(1);
|
||||||
|
|
||||||
|
const className = $div[0].className;
|
||||||
|
|
||||||
|
expect(className).to.match(/heading-/);
|
||||||
})
|
})
|
||||||
|
// .then(cb) callback is not retried,
|
||||||
|
// it either passes or fails
|
||||||
|
.then(($div) => {
|
||||||
|
expect($div, "text content").to.have.text("Introduction");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('.and() - chain multiple assertions together', () => {
|
it("can throw any error", () => {
|
||||||
// https://on.cypress.io/and
|
cy.get(".docs-header")
|
||||||
cy.get('.assertions-link')
|
.find("div")
|
||||||
.should('have.class', 'active')
|
.should(($div) => {
|
||||||
.and('have.attr', 'href')
|
if ($div.length !== 1) {
|
||||||
.and('include', 'cypress.io')
|
// you can throw your own errors
|
||||||
})
|
throw new Error("Did not find 1 element");
|
||||||
})
|
}
|
||||||
|
|
||||||
describe('Explicit Assertions', () => {
|
const className = $div[0].className;
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
it('expect - make an assertion about a specified subject', () => {
|
|
||||||
// We can use Chai's BDD style assertions
|
|
||||||
expect(true).to.be.true
|
|
||||||
const o = {foo: 'bar'}
|
|
||||||
|
|
||||||
expect(o).to.equal(o)
|
if (!className.match(/heading-/)) {
|
||||||
expect(o).to.deep.equal({foo: 'bar'})
|
throw new Error(`Could not find class "heading-" in ${className}`);
|
||||||
// matching text using regular expression
|
}
|
||||||
expect('FooBar').to.match(/bar$/i)
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('pass your own callback function to should()', () => {
|
it("matches unknown text between two elements", () => {
|
||||||
// Pass a function to should that can have any number
|
/**
|
||||||
// of explicit assertions within it.
|
* Text from the first element.
|
||||||
// The ".should(cb)" function will be retried
|
* @type {string}
|
||||||
// automatically until it passes all your explicit assertions or times out.
|
*/
|
||||||
cy.get('.assertions-p')
|
let text;
|
||||||
.find('p')
|
|
||||||
.should(($p) => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
// return an array of texts from all of the p's
|
|
||||||
// @ts-ignore TS6133 unused variable
|
|
||||||
const texts = $p.map((i, el) => Cypress.$(el).text())
|
|
||||||
|
|
||||||
// jquery map returns jquery object
|
/**
|
||||||
// and .get() convert this to simple array
|
* Normalizes passed text,
|
||||||
const paragraphs = texts.get()
|
* useful before comparing text with spaces and different capitalization.
|
||||||
|
* @param {string} s Text to normalize
|
||||||
|
*/
|
||||||
|
const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
|
||||||
|
|
||||||
// array should have length of 3
|
cy.get(".two-elements")
|
||||||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
|
.find(".first")
|
||||||
|
.then(($first) => {
|
||||||
|
// save text from the first element
|
||||||
|
text = normalizeText($first.text());
|
||||||
|
});
|
||||||
|
|
||||||
// use second argument to expect(...) to provide clear
|
cy.get(".two-elements")
|
||||||
// message with each assertion
|
.find(".second")
|
||||||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
|
.should(($div) => {
|
||||||
'Some text from first p',
|
// we can massage text before comparing
|
||||||
'More text from second p',
|
const secondText = normalizeText($div.text());
|
||||||
'And even more text from third p',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('finds element by class name regex', () => {
|
expect(secondText, "second text").to.equal(text);
|
||||||
cy.get('.docs-header')
|
});
|
||||||
.find('div')
|
});
|
||||||
// .should(cb) callback function will be retried
|
|
||||||
.should(($div) => {
|
|
||||||
expect($div).to.have.length(1)
|
|
||||||
|
|
||||||
const className = $div[0].className
|
it("assert - assert shape of an object", () => {
|
||||||
|
const person = {
|
||||||
|
name: "Joe",
|
||||||
|
age: 20
|
||||||
|
};
|
||||||
|
|
||||||
expect(className).to.match(/heading-/)
|
assert.isObject(person, "value is object");
|
||||||
})
|
});
|
||||||
// .then(cb) callback is not retried,
|
|
||||||
// it either passes or fails
|
|
||||||
.then(($div) => {
|
|
||||||
expect($div, 'text content').to.have.text('Introduction')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can throw any error', () => {
|
it("retries the should callback until assertions pass", () => {
|
||||||
cy.get('.docs-header')
|
cy.get("#random-number").should(($div) => {
|
||||||
.find('div')
|
const n = parseFloat($div.text());
|
||||||
.should(($div) => {
|
|
||||||
if ($div.length !== 1) {
|
|
||||||
// you can throw your own errors
|
|
||||||
throw new Error('Did not find 1 element')
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = $div[0].className
|
expect(n).to.be.gte(1).and.be.lte(10);
|
||||||
|
});
|
||||||
if (!className.match(/heading-/)) {
|
});
|
||||||
throw new Error(`Could not find class "heading-" in ${className}`)
|
});
|
||||||
}
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches unknown text between two elements', () => {
|
|
||||||
/**
|
|
||||||
* Text from the first element.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let text
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes passed text,
|
|
||||||
* useful before comparing text with spaces and different capitalization.
|
|
||||||
* @param {string} s Text to normalize
|
|
||||||
*/
|
|
||||||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
|
|
||||||
|
|
||||||
cy.get('.two-elements')
|
|
||||||
.find('.first')
|
|
||||||
.then(($first) => {
|
|
||||||
// save text from the first element
|
|
||||||
text = normalizeText($first.text())
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('.two-elements')
|
|
||||||
.find('.second')
|
|
||||||
.should(($div) => {
|
|
||||||
// we can massage text before comparing
|
|
||||||
const secondText = normalizeText($div.text())
|
|
||||||
|
|
||||||
expect(secondText, 'second text').to.equal(text)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('assert - assert shape of an object', () => {
|
|
||||||
const person = {
|
|
||||||
name: 'Joe',
|
|
||||||
age: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.isObject(person, 'value is object')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('retries the should callback until assertions pass', () => {
|
|
||||||
cy.get('#random-number')
|
|
||||||
.should(($div) => {
|
|
||||||
const n = parseFloat($div.text())
|
|
||||||
|
|
||||||
expect(n).to.be.gte(1).and.be.lte(10)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,97 +1,96 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Connectors', () => {
|
context("Connectors", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/connectors')
|
cy.visit("https://example.cypress.io/commands/connectors");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.each() - iterate over an array of elements', () => {
|
it(".each() - iterate over an array of elements", () => {
|
||||||
// https://on.cypress.io/each
|
// https://on.cypress.io/each
|
||||||
cy.get('.connectors-each-ul>li')
|
cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
|
||||||
.each(($el, index, $list) => {
|
console.log($el, index, $list);
|
||||||
console.log($el, index, $list)
|
});
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.its() - get properties on the current subject', () => {
|
it(".its() - get properties on the current subject", () => {
|
||||||
// https://on.cypress.io/its
|
// https://on.cypress.io/its
|
||||||
cy.get('.connectors-its-ul>li')
|
cy.get(".connectors-its-ul>li")
|
||||||
// calls the 'length' property yielding that value
|
// calls the 'length' property yielding that value
|
||||||
.its('length')
|
.its("length")
|
||||||
.should('be.gt', 2)
|
.should("be.gt", 2);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.invoke() - invoke a function on the current subject', () => {
|
it(".invoke() - invoke a function on the current subject", () => {
|
||||||
// our div is hidden in our script.js
|
// our div is hidden in our script.js
|
||||||
// $('.connectors-div').hide()
|
// $('.connectors-div').hide()
|
||||||
|
|
||||||
// https://on.cypress.io/invoke
|
// https://on.cypress.io/invoke
|
||||||
cy.get('.connectors-div').should('be.hidden')
|
cy.get(".connectors-div")
|
||||||
// call the jquery method 'show' on the 'div.container'
|
.should("be.hidden")
|
||||||
.invoke('show')
|
// call the jquery method 'show' on the 'div.container'
|
||||||
.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
|
// https://on.cypress.io/spread
|
||||||
const arr = ['foo', 'bar', 'baz']
|
const arr = ["foo", "bar", "baz"];
|
||||||
|
|
||||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
cy.wrap(arr).spread((foo, bar, baz) => {
|
||||||
expect(foo).to.eq('foo')
|
expect(foo).to.eq("foo");
|
||||||
expect(bar).to.eq('bar')
|
expect(bar).to.eq("bar");
|
||||||
expect(baz).to.eq('baz')
|
expect(baz).to.eq("baz");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(".then()", () => {
|
||||||
|
it("invokes a callback function with the current subject", () => {
|
||||||
|
// https://on.cypress.io/then
|
||||||
|
cy.get(".connectors-list > li").then(($lis) => {
|
||||||
|
expect($lis, "3 items").to.have.length(3);
|
||||||
|
expect($lis.eq(0), "first item").to.contain("Walk the dog");
|
||||||
|
expect($lis.eq(1), "second item").to.contain("Feed the cat");
|
||||||
|
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("yields the returned value to the next command", () => {
|
||||||
|
cy.wrap(1)
|
||||||
|
.then((num) => {
|
||||||
|
expect(num).to.equal(1);
|
||||||
|
|
||||||
|
return 2;
|
||||||
})
|
})
|
||||||
})
|
.then((num) => {
|
||||||
|
expect(num).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('.then()', () => {
|
it("yields the original subject without return", () => {
|
||||||
it('invokes a callback function with the current subject', () => {
|
cy.wrap(1)
|
||||||
// https://on.cypress.io/then
|
.then((num) => {
|
||||||
cy.get('.connectors-list > li')
|
expect(num).to.equal(1);
|
||||||
.then(($lis) => {
|
// note that nothing is returned from this callback
|
||||||
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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
.then((num) => {
|
||||||
|
// this callback receives the original unchanged value 1
|
||||||
|
expect(num).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('yields the returned value to the next command', () => {
|
it("yields the value yielded by the last Cypress command inside", () => {
|
||||||
cy.wrap(1)
|
cy.wrap(1)
|
||||||
.then((num) => {
|
.then((num) => {
|
||||||
expect(num).to.equal(1)
|
expect(num).to.equal(1);
|
||||||
|
// note how we run a Cypress command
|
||||||
return 2
|
// the result yielded by this Cypress command
|
||||||
})
|
// will be passed to the second ".then"
|
||||||
.then((num) => {
|
cy.wrap(2);
|
||||||
expect(num).to.equal(2)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
.then((num) => {
|
||||||
it('yields the original subject without return', () => {
|
// this callback receives the value yielded by "cy.wrap(2)"
|
||||||
cy.wrap(1)
|
expect(num).to.equal(2);
|
||||||
.then((num) => {
|
});
|
||||||
expect(num).to.equal(1)
|
});
|
||||||
// note that nothing is returned from this callback
|
});
|
||||||
})
|
});
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the original unchanged value 1
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('yields the value yielded by the last Cypress command inside', () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
// note how we run a Cypress command
|
|
||||||
// the result yielded by this Cypress command
|
|
||||||
// will be passed to the second ".then"
|
|
||||||
cy.wrap(2)
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the value yielded by "cy.wrap(2)"
|
|
||||||
expect(num).to.equal(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,77 +1,79 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Cookies', () => {
|
context("Cookies", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Cypress.Cookies.debug(true)
|
Cypress.Cookies.debug(true);
|
||||||
|
|
||||||
cy.visit('https://example.cypress.io/commands/cookies')
|
cy.visit("https://example.cypress.io/commands/cookies");
|
||||||
|
|
||||||
// clear cookies again after visiting to remove
|
// clear cookies again after visiting to remove
|
||||||
// any 3rd party cookies picked up such as cloudflare
|
// any 3rd party cookies picked up such as cloudflare
|
||||||
cy.clearCookies()
|
cy.clearCookies();
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.getCookie() - get a browser cookie', () => {
|
it("cy.getCookie() - get a browser cookie", () => {
|
||||||
// https://on.cypress.io/getcookie
|
// https://on.cypress.io/getcookie
|
||||||
cy.get('#getCookie .set-a-cookie').click()
|
cy.get("#getCookie .set-a-cookie").click();
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
// cy.getCookie() yields a cookie object
|
||||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
cy.getCookie("token").should("have.property", "value", "123ABC");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.getCookies() - get browser cookies', () => {
|
it("cy.getCookies() - get browser cookies", () => {
|
||||||
// https://on.cypress.io/getcookies
|
// https://on.cypress.io/getcookies
|
||||||
cy.getCookies().should('be.empty')
|
cy.getCookies().should("be.empty");
|
||||||
|
|
||||||
cy.get('#getCookies .set-a-cookie').click()
|
cy.get("#getCookies .set-a-cookie").click();
|
||||||
|
|
||||||
// cy.getCookies() yields an array of cookies
|
// cy.getCookies() yields an array of cookies
|
||||||
cy.getCookies().should('have.length', 1).should((cookies) => {
|
cy.getCookies()
|
||||||
// each cookie has these properties
|
.should("have.length", 1)
|
||||||
expect(cookies[0]).to.have.property('name', 'token')
|
.should((cookies) => {
|
||||||
expect(cookies[0]).to.have.property('value', '123ABC')
|
// each cookie has these properties
|
||||||
expect(cookies[0]).to.have.property('httpOnly', false)
|
expect(cookies[0]).to.have.property("name", "token");
|
||||||
expect(cookies[0]).to.have.property('secure', false)
|
expect(cookies[0]).to.have.property("value", "123ABC");
|
||||||
expect(cookies[0]).to.have.property('domain')
|
expect(cookies[0]).to.have.property("httpOnly", false);
|
||||||
expect(cookies[0]).to.have.property('path')
|
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
|
// https://on.cypress.io/setcookie
|
||||||
cy.getCookies().should('be.empty')
|
cy.getCookies().should("be.empty");
|
||||||
|
|
||||||
cy.setCookie('foo', 'bar')
|
cy.setCookie("foo", "bar");
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
// cy.getCookie() yields a cookie object
|
||||||
cy.getCookie('foo').should('have.property', 'value', 'bar')
|
cy.getCookie("foo").should("have.property", "value", "bar");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.clearCookie() - clear a browser cookie', () => {
|
it("cy.clearCookie() - clear a browser cookie", () => {
|
||||||
// https://on.cypress.io/clearcookie
|
// https://on.cypress.io/clearcookie
|
||||||
cy.getCookie('token').should('be.null')
|
cy.getCookie("token").should("be.null");
|
||||||
|
|
||||||
cy.get('#clearCookie .set-a-cookie').click()
|
cy.get("#clearCookie .set-a-cookie").click();
|
||||||
|
|
||||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
cy.getCookie("token").should("have.property", "value", "123ABC");
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
// cy.clearCookies() yields null
|
||||||
cy.clearCookie('token').should('be.null')
|
cy.clearCookie("token").should("be.null");
|
||||||
|
|
||||||
cy.getCookie('token').should('be.null')
|
cy.getCookie("token").should("be.null");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.clearCookies() - clear browser cookies', () => {
|
it("cy.clearCookies() - clear browser cookies", () => {
|
||||||
// https://on.cypress.io/clearcookies
|
// https://on.cypress.io/clearcookies
|
||||||
cy.getCookies().should('be.empty')
|
cy.getCookies().should("be.empty");
|
||||||
|
|
||||||
cy.get('#clearCookies .set-a-cookie').click()
|
cy.get("#clearCookies .set-a-cookie").click();
|
||||||
|
|
||||||
cy.getCookies().should('have.length', 1)
|
cy.getCookies().should("have.length", 1);
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
// cy.clearCookies() yields null
|
||||||
cy.clearCookies()
|
cy.clearCookies();
|
||||||
|
|
||||||
cy.getCookies().should('be.empty')
|
cy.getCookies().should("be.empty");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,202 +1,208 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Cypress.Commands', () => {
|
context("Cypress.Commands", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
// https://on.cypress.io/custom-commands
|
// https://on.cypress.io/custom-commands
|
||||||
|
|
||||||
it('.add() - create a custom command', () => {
|
it(".add() - create a custom command", () => {
|
||||||
Cypress.Commands.add('console', {
|
Cypress.Commands.add(
|
||||||
prevSubject: true,
|
"console",
|
||||||
}, (subject, method) => {
|
{
|
||||||
// the previous subject is automatically received
|
prevSubject: true
|
||||||
// and the commands arguments are shifted
|
},
|
||||||
|
(subject, method) => {
|
||||||
|
// the previous subject is automatically received
|
||||||
|
// and the commands arguments are shifted
|
||||||
|
|
||||||
// allow us to change the console method used
|
// allow us to change the console method used
|
||||||
method = method || 'log'
|
method = method || "log";
|
||||||
|
|
||||||
// log the subject to the console
|
// log the subject to the console
|
||||||
// @ts-ignore TS7017
|
// @ts-ignore TS7017
|
||||||
console[method]('The subject is', subject)
|
console[method]("The subject is", subject);
|
||||||
|
|
||||||
// whatever we return becomes the new subject
|
// whatever we return becomes the new subject
|
||||||
// we don't want to change the subject so
|
// we don't want to change the subject so
|
||||||
// we return whatever was passed in
|
// we return whatever was passed in
|
||||||
return subject
|
return subject;
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-ignore TS2339
|
// @ts-ignore TS2339
|
||||||
cy.get('button').console('info').then(($button) => {
|
cy.get("button")
|
||||||
// subject is still $button
|
.console("info")
|
||||||
})
|
.then(($button) => {
|
||||||
})
|
// subject is still $button
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
context('Cypress.Cookies', () => {
|
context("Cypress.Cookies", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
// https://on.cypress.io/cookies
|
// https://on.cypress.io/cookies
|
||||||
it('.debug() - enable or disable debugging', () => {
|
it(".debug() - enable or disable debugging", () => {
|
||||||
Cypress.Cookies.debug(true)
|
Cypress.Cookies.debug(true);
|
||||||
|
|
||||||
// Cypress will now log in the console when
|
// Cypress will now log in the console when
|
||||||
// cookies are set or cleared
|
// cookies are set or cleared
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
cy.setCookie("fakeCookie", "123ABC");
|
||||||
cy.clearCookie('fakeCookie')
|
cy.clearCookie("fakeCookie");
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
cy.setCookie("fakeCookie", "123ABC");
|
||||||
cy.clearCookie('fakeCookie')
|
cy.clearCookie("fakeCookie");
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
cy.setCookie("fakeCookie", "123ABC");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.preserveOnce() - preserve cookies by key', () => {
|
it(".preserveOnce() - preserve cookies by key", () => {
|
||||||
// normally cookies are reset after each test
|
// normally cookies are reset after each test
|
||||||
cy.getCookie('fakeCookie').should('not.be.ok')
|
cy.getCookie("fakeCookie").should("not.be.ok");
|
||||||
|
|
||||||
// preserving a cookie will not clear it when
|
// preserving a cookie will not clear it when
|
||||||
// the next test starts
|
// the next test starts
|
||||||
cy.setCookie('lastCookie', '789XYZ')
|
cy.setCookie("lastCookie", "789XYZ");
|
||||||
Cypress.Cookies.preserveOnce('lastCookie')
|
Cypress.Cookies.preserveOnce("lastCookie");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.defaults() - set defaults for all cookies', () => {
|
it(".defaults() - set defaults for all cookies", () => {
|
||||||
// now any cookie with the name 'session_id' will
|
// now any cookie with the name 'session_id' will
|
||||||
// not be cleared before each new test runs
|
// not be cleared before each new test runs
|
||||||
Cypress.Cookies.defaults({
|
Cypress.Cookies.defaults({
|
||||||
preserve: 'session_id',
|
preserve: "session_id"
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.arch', () => {
|
context("Cypress.arch", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Get CPU architecture name of underlying OS', () => {
|
it("Get CPU architecture name of underlying OS", () => {
|
||||||
// https://on.cypress.io/arch
|
// https://on.cypress.io/arch
|
||||||
expect(Cypress.arch).to.exist
|
expect(Cypress.arch).to.exist;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.config()', () => {
|
context("Cypress.config()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Get and set configuration options', () => {
|
it("Get and set configuration options", () => {
|
||||||
// https://on.cypress.io/config
|
// https://on.cypress.io/config
|
||||||
let myConfig = Cypress.config()
|
let myConfig = Cypress.config();
|
||||||
|
|
||||||
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
|
expect(myConfig).to.have.property("animationDistanceThreshold", 5);
|
||||||
expect(myConfig).to.have.property('baseUrl', null)
|
expect(myConfig).to.have.property("baseUrl", null);
|
||||||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
|
expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
|
||||||
expect(myConfig).to.have.property('requestTimeout', 5000)
|
expect(myConfig).to.have.property("requestTimeout", 5000);
|
||||||
expect(myConfig).to.have.property('responseTimeout', 30000)
|
expect(myConfig).to.have.property("responseTimeout", 30000);
|
||||||
expect(myConfig).to.have.property('viewportHeight', 660)
|
expect(myConfig).to.have.property("viewportHeight", 660);
|
||||||
expect(myConfig).to.have.property('viewportWidth', 1000)
|
expect(myConfig).to.have.property("viewportWidth", 1000);
|
||||||
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
|
expect(myConfig).to.have.property("pageLoadTimeout", 60000);
|
||||||
expect(myConfig).to.have.property('waitForAnimations', true)
|
expect(myConfig).to.have.property("waitForAnimations", true);
|
||||||
|
|
||||||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
|
expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
|
||||||
|
|
||||||
// this will change the config for the rest of your tests!
|
// this will change the config for the rest of your tests!
|
||||||
Cypress.config('pageLoadTimeout', 20000)
|
Cypress.config("pageLoadTimeout", 20000);
|
||||||
|
|
||||||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
|
expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
|
||||||
|
|
||||||
Cypress.config('pageLoadTimeout', 60000)
|
Cypress.config("pageLoadTimeout", 60000);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.dom', () => {
|
context("Cypress.dom", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
// https://on.cypress.io/dom
|
// https://on.cypress.io/dom
|
||||||
it('.isHidden() - determine if a DOM element is hidden', () => {
|
it(".isHidden() - determine if a DOM element is hidden", () => {
|
||||||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
|
let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
|
||||||
let visibleP = Cypress.$('.dom-p p.visible').get(0)
|
let visibleP = Cypress.$(".dom-p p.visible").get(0);
|
||||||
|
|
||||||
// our first paragraph has css class 'hidden'
|
// our first paragraph has css class 'hidden'
|
||||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
|
expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
|
||||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false
|
expect(Cypress.dom.isHidden(visibleP)).to.be.false;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.env()', () => {
|
context("Cypress.env()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
// We can set environment variables for highly dynamic values
|
// We can set environment variables for highly dynamic values
|
||||||
|
|
||||||
// https://on.cypress.io/environment-variables
|
// https://on.cypress.io/environment-variables
|
||||||
it('Get environment variables', () => {
|
it("Get environment variables", () => {
|
||||||
// https://on.cypress.io/env
|
// https://on.cypress.io/env
|
||||||
// set multiple environment variables
|
// set multiple environment variables
|
||||||
Cypress.env({
|
Cypress.env({
|
||||||
host: 'veronica.dev.local',
|
host: "veronica.dev.local",
|
||||||
api_server: 'http://localhost:8888/v1/',
|
api_server: "http://localhost:8888/v1/"
|
||||||
})
|
});
|
||||||
|
|
||||||
// get environment variable
|
// get environment variable
|
||||||
expect(Cypress.env('host')).to.eq('veronica.dev.local')
|
expect(Cypress.env("host")).to.eq("veronica.dev.local");
|
||||||
|
|
||||||
// set environment variable
|
// set environment variable
|
||||||
Cypress.env('api_server', 'http://localhost:8888/v2/')
|
Cypress.env("api_server", "http://localhost:8888/v2/");
|
||||||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
|
expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
|
||||||
|
|
||||||
// get all environment variable
|
// get all environment variable
|
||||||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
|
expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
|
||||||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
|
expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.log', () => {
|
context("Cypress.log", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Control what is printed to the Command Log', () => {
|
it("Control what is printed to the Command Log", () => {
|
||||||
// https://on.cypress.io/cypress-log
|
// https://on.cypress.io/cypress-log
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.platform', () => {
|
context("Cypress.platform", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Get underlying OS name', () => {
|
it("Get underlying OS name", () => {
|
||||||
// https://on.cypress.io/platform
|
// https://on.cypress.io/platform
|
||||||
expect(Cypress.platform).to.be.exist
|
expect(Cypress.platform).to.be.exist;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.version', () => {
|
context("Cypress.version", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Get current version of Cypress being run', () => {
|
it("Get current version of Cypress being run", () => {
|
||||||
// https://on.cypress.io/version
|
// https://on.cypress.io/version
|
||||||
expect(Cypress.version).to.be.exist
|
expect(Cypress.version).to.be.exist;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.spec', () => {
|
context("Cypress.spec", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
cy.visit("https://example.cypress.io/cypress-api");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Get current spec information', () => {
|
it("Get current spec information", () => {
|
||||||
// https://on.cypress.io/spec
|
// https://on.cypress.io/spec
|
||||||
// wrap the object so we can inspect it easily by clicking in the command log
|
// wrap the object so we can inspect it easily by clicking in the command log
|
||||||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
|
cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -3,86 +3,84 @@
|
|||||||
/// JSON fixture file can be loaded directly using
|
/// JSON fixture file can be loaded directly using
|
||||||
// the built-in JavaScript bundler
|
// the built-in JavaScript bundler
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const requiredExample = require('../../fixtures/example')
|
const requiredExample = require("../../fixtures/example");
|
||||||
|
|
||||||
context('Files', () => {
|
context("Files", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/files')
|
cy.visit("https://example.cypress.io/commands/files");
|
||||||
})
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// load example.json fixture file and store
|
// load example.json fixture file and store
|
||||||
// in the test context object
|
// in the test context object
|
||||||
cy.fixture('example.json').as('example')
|
cy.fixture("example.json").as("example");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.fixture() - load a fixture', () => {
|
it("cy.fixture() - load a fixture", () => {
|
||||||
// https://on.cypress.io/fixture
|
// https://on.cypress.io/fixture
|
||||||
|
|
||||||
// Instead of writing a response inline you can
|
// Instead of writing a response inline you can
|
||||||
// use a fixture file's content.
|
// use a fixture file's content.
|
||||||
|
|
||||||
// when application makes an Ajax request matching "GET **/comments/*"
|
// when application makes an Ajax request matching "GET **/comments/*"
|
||||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
// Cypress will intercept it and reply with the object in `example.json` fixture
|
||||||
cy.intercept('GET', '**/comments/*', {fixture: 'example.json'}).as('getComment')
|
cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
|
||||||
|
|
||||||
// we have code that gets a comment when
|
// we have code that gets a comment when
|
||||||
// the button is clicked in scripts.js
|
// the button is clicked in scripts.js
|
||||||
cy.get('.fixture-btn').click()
|
cy.get(".fixture-btn").click();
|
||||||
|
|
||||||
cy.wait('@getComment').its('response.body')
|
cy.wait("@getComment")
|
||||||
.should('have.property', 'name')
|
.its("response.body")
|
||||||
.and('include', 'Using fixtures to represent data')
|
.should("have.property", "name")
|
||||||
})
|
.and("include", "Using fixtures to represent data");
|
||||||
|
});
|
||||||
|
|
||||||
it('cy.fixture() or require - load a fixture', function () {
|
it("cy.fixture() or require - load a fixture", function () {
|
||||||
// we are inside the "function () { ... }"
|
// we are inside the "function () { ... }"
|
||||||
// callback and can use test context object "this"
|
// callback and can use test context object "this"
|
||||||
// "this.example" was loaded in "beforeEach" function callback
|
// "this.example" was loaded in "beforeEach" function callback
|
||||||
expect(this.example, 'fixture in the test context')
|
expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
|
||||||
.to.deep.equal(requiredExample)
|
|
||||||
|
|
||||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
||||||
cy.wrap(this.example)
|
cy.wrap(this.example).should("deep.equal", requiredExample);
|
||||||
.should('deep.equal', requiredExample)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.readFile() - read file contents', () => {
|
it("cy.readFile() - read file contents", () => {
|
||||||
// https://on.cypress.io/readfile
|
// https://on.cypress.io/readfile
|
||||||
|
|
||||||
// You can read a file and yield its contents
|
// You can read a file and yield its contents
|
||||||
// The filePath is relative to your project's root.
|
// The filePath is relative to your project's root.
|
||||||
cy.readFile('cypress.json').then((json) => {
|
cy.readFile("cypress.json").then((json) => {
|
||||||
expect(json).to.be.an('object')
|
expect(json).to.be.an("object");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.writeFile() - write to a file', () => {
|
it("cy.writeFile() - write to a file", () => {
|
||||||
// https://on.cypress.io/writefile
|
// https://on.cypress.io/writefile
|
||||||
|
|
||||||
// You can write to a file
|
// You can write to a file
|
||||||
|
|
||||||
// Use a response from a request to automatically
|
// Use a response from a request to automatically
|
||||||
// generate a fixture file for use later
|
// generate a fixture file for use later
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
||||||
.then((response) => {
|
cy.writeFile("cypress/fixtures/users.json", response.body);
|
||||||
cy.writeFile('cypress/fixtures/users.json', response.body)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
cy.fixture('users').should((users) => {
|
cy.fixture("users").should((users) => {
|
||||||
expect(users[0].name).to.exist
|
expect(users[0].name).to.exist;
|
||||||
})
|
});
|
||||||
|
|
||||||
// JavaScript arrays and objects are stringified
|
// JavaScript arrays and objects are stringified
|
||||||
// and formatted into text.
|
// and formatted into text.
|
||||||
cy.writeFile('cypress/fixtures/profile.json', {
|
cy.writeFile("cypress/fixtures/profile.json", {
|
||||||
id: 8739,
|
id: 8739,
|
||||||
name: 'Jane',
|
name: "Jane",
|
||||||
email: 'jane@example.com',
|
email: "jane@example.com"
|
||||||
})
|
});
|
||||||
|
|
||||||
cy.fixture('profile').should((profile) => {
|
cy.fixture("profile").should((profile) => {
|
||||||
expect(profile.name).to.eq('Jane')
|
expect(profile.name).to.eq("Jane");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,52 +1,58 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Local Storage', () => {
|
context("Local Storage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/local-storage')
|
cy.visit("https://example.cypress.io/commands/local-storage");
|
||||||
})
|
});
|
||||||
// Although local storage is automatically cleared
|
// Although local storage is automatically cleared
|
||||||
// in between tests to maintain a clean state
|
// in between tests to maintain a clean state
|
||||||
// sometimes we need to clear the local storage manually
|
// sometimes we need to clear the local storage manually
|
||||||
|
|
||||||
it('cy.clearLocalStorage() - clear all data in local storage', () => {
|
it("cy.clearLocalStorage() - clear all data in local storage", () => {
|
||||||
// https://on.cypress.io/clearlocalstorage
|
// https://on.cypress.io/clearlocalstorage
|
||||||
cy.get('.ls-btn').click().should(() => {
|
cy.get(".ls-btn")
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
.click()
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
.should(() => {
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
expect(localStorage.getItem("prop1")).to.eq("red");
|
||||||
})
|
expect(localStorage.getItem("prop2")).to.eq("blue");
|
||||||
|
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
||||||
|
});
|
||||||
|
|
||||||
// clearLocalStorage() yields the localStorage object
|
// clearLocalStorage() yields the localStorage object
|
||||||
cy.clearLocalStorage().should((ls) => {
|
cy.clearLocalStorage().should((ls) => {
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
expect(ls.getItem("prop1")).to.be.null;
|
||||||
expect(ls.getItem('prop2')).to.be.null
|
expect(ls.getItem("prop2")).to.be.null;
|
||||||
expect(ls.getItem('prop3')).to.be.null
|
expect(ls.getItem("prop3")).to.be.null;
|
||||||
})
|
});
|
||||||
|
|
||||||
cy.get('.ls-btn').click().should(() => {
|
cy.get(".ls-btn")
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
.click()
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
.should(() => {
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
expect(localStorage.getItem("prop1")).to.eq("red");
|
||||||
})
|
expect(localStorage.getItem("prop2")).to.eq("blue");
|
||||||
|
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
||||||
|
});
|
||||||
|
|
||||||
// Clear key matching string in Local Storage
|
// Clear key matching string in Local Storage
|
||||||
cy.clearLocalStorage('prop1').should((ls) => {
|
cy.clearLocalStorage("prop1").should((ls) => {
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
expect(ls.getItem("prop1")).to.be.null;
|
||||||
expect(ls.getItem('prop2')).to.eq('blue')
|
expect(ls.getItem("prop2")).to.eq("blue");
|
||||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
expect(ls.getItem("prop3")).to.eq("magenta");
|
||||||
})
|
});
|
||||||
|
|
||||||
cy.get('.ls-btn').click().should(() => {
|
cy.get(".ls-btn")
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
.click()
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
.should(() => {
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
expect(localStorage.getItem("prop1")).to.eq("red");
|
||||||
})
|
expect(localStorage.getItem("prop2")).to.eq("blue");
|
||||||
|
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
||||||
|
});
|
||||||
|
|
||||||
// Clear keys matching regex in Local Storage
|
// Clear keys matching regex in Local Storage
|
||||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
expect(ls.getItem("prop1")).to.be.null;
|
||||||
expect(ls.getItem('prop2')).to.be.null
|
expect(ls.getItem("prop2")).to.be.null;
|
||||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
expect(ls.getItem("prop3")).to.eq("magenta");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Location', () => {
|
context("Location", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/location')
|
cy.visit("https://example.cypress.io/commands/location");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.hash() - get the current URL hash', () => {
|
it("cy.hash() - get the current URL hash", () => {
|
||||||
// https://on.cypress.io/hash
|
// https://on.cypress.io/hash
|
||||||
cy.hash().should('be.empty')
|
cy.hash().should("be.empty");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.location() - get window.location', () => {
|
it("cy.location() - get window.location", () => {
|
||||||
// https://on.cypress.io/location
|
// https://on.cypress.io/location
|
||||||
cy.location().should((location) => {
|
cy.location().should((location) => {
|
||||||
expect(location.hash).to.be.empty
|
expect(location.hash).to.be.empty;
|
||||||
expect(location.href).to.eq('https://example.cypress.io/commands/location')
|
expect(location.href).to.eq("https://example.cypress.io/commands/location");
|
||||||
expect(location.host).to.eq('example.cypress.io')
|
expect(location.host).to.eq("example.cypress.io");
|
||||||
expect(location.hostname).to.eq('example.cypress.io')
|
expect(location.hostname).to.eq("example.cypress.io");
|
||||||
expect(location.origin).to.eq('https://example.cypress.io')
|
expect(location.origin).to.eq("https://example.cypress.io");
|
||||||
expect(location.pathname).to.eq('/commands/location')
|
expect(location.pathname).to.eq("/commands/location");
|
||||||
expect(location.port).to.eq('')
|
expect(location.port).to.eq("");
|
||||||
expect(location.protocol).to.eq('https:')
|
expect(location.protocol).to.eq("https:");
|
||||||
expect(location.search).to.be.empty
|
expect(location.search).to.be.empty;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.url() - get the current URL', () => {
|
it("cy.url() - get the current URL", () => {
|
||||||
// https://on.cypress.io/url
|
// https://on.cypress.io/url
|
||||||
cy.url().should('eq', 'https://example.cypress.io/commands/location')
|
cy.url().should("eq", "https://example.cypress.io/commands/location");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,106 +1,98 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Misc', () => {
|
context("Misc", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/misc')
|
cy.visit("https://example.cypress.io/commands/misc");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.end() - end the command chain', () => {
|
it(".end() - end the command chain", () => {
|
||||||
// https://on.cypress.io/end
|
// https://on.cypress.io/end
|
||||||
|
|
||||||
// cy.end is useful when you want to end a chain of commands
|
// cy.end is useful when you want to end a chain of commands
|
||||||
// and force Cypress to re-query from the root element
|
// and force Cypress to re-query from the root element
|
||||||
cy.get('.misc-table').within(() => {
|
cy.get(".misc-table").within(() => {
|
||||||
// ends the current chain and yields null
|
// ends the current chain and yields null
|
||||||
cy.contains('Cheryl').click().end()
|
cy.contains("Cheryl").click().end();
|
||||||
|
|
||||||
// queries the entire table again
|
// queries the entire table again
|
||||||
cy.contains('Charles').click()
|
cy.contains("Charles").click();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.exec() - execute a system command', () => {
|
it("cy.exec() - execute a system command", () => {
|
||||||
// execute a system command.
|
// execute a system command.
|
||||||
// so you can take actions necessary for
|
// so you can take actions necessary for
|
||||||
// your test outside the scope of Cypress.
|
// your test outside the scope of Cypress.
|
||||||
// https://on.cypress.io/exec
|
// https://on.cypress.io/exec
|
||||||
|
|
||||||
// we can use Cypress.platform string to
|
// we can use Cypress.platform string to
|
||||||
// select appropriate command
|
// select appropriate command
|
||||||
// https://on.cypress/io/platform
|
// https://on.cypress/io/platform
|
||||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
|
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
|
||||||
|
|
||||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
// on CircleCI Windows build machines we have a failure to run bash shell
|
||||||
// https://github.com/cypress-io/cypress/issues/5169
|
// https://github.com/cypress-io/cypress/issues/5169
|
||||||
// so skip some of the tests by passing flag "--env circle=true"
|
// so skip some of the tests by passing flag "--env circle=true"
|
||||||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
|
const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
|
||||||
|
|
||||||
if (isCircleOnWindows) {
|
if (isCircleOnWindows) {
|
||||||
cy.log('Skipping test on CircleCI')
|
cy.log("Skipping test on CircleCI");
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cy.exec problem on Shippable CI
|
// cy.exec problem on Shippable CI
|
||||||
// https://github.com/cypress-io/cypress/issues/6718
|
// https://github.com/cypress-io/cypress/issues/6718
|
||||||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
|
const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
|
||||||
|
|
||||||
if (isShippable) {
|
if (isShippable) {
|
||||||
cy.log('Skipping test on ShippableCI')
|
cy.log("Skipping test on ShippableCI");
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.exec('echo Jane Lane')
|
cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
|
||||||
.its('stdout').should('contain', 'Jane Lane')
|
|
||||||
|
|
||||||
if (Cypress.platform === 'win32') {
|
if (Cypress.platform === "win32") {
|
||||||
cy.exec('print cypress.json')
|
cy.exec("print cypress.json").its("stderr").should("be.empty");
|
||||||
.its('stderr').should('be.empty')
|
} else {
|
||||||
} else {
|
cy.exec("cat cypress.json").its("stderr").should("be.empty");
|
||||||
cy.exec('cat cypress.json')
|
|
||||||
.its('stderr').should('be.empty')
|
|
||||||
|
|
||||||
cy.exec('pwd')
|
cy.exec("pwd").its("code").should("eq", 0);
|
||||||
.its('code').should('eq', 0)
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.focused() - get the DOM element that has focus', () => {
|
it("cy.focused() - get the DOM element that has focus", () => {
|
||||||
// https://on.cypress.io/focused
|
// https://on.cypress.io/focused
|
||||||
cy.get('.misc-form').find('#name').click()
|
cy.get(".misc-form").find("#name").click();
|
||||||
cy.focused().should('have.id', 'name')
|
cy.focused().should("have.id", "name");
|
||||||
|
|
||||||
cy.get('.misc-form').find('#description').click()
|
cy.get(".misc-form").find("#description").click();
|
||||||
cy.focused().should('have.id', 'description')
|
cy.focused().should("have.id", "description");
|
||||||
})
|
});
|
||||||
|
|
||||||
context('Cypress.Screenshot', function () {
|
context("Cypress.Screenshot", function () {
|
||||||
it('cy.screenshot() - take a screenshot', () => {
|
it("cy.screenshot() - take a screenshot", () => {
|
||||||
// https://on.cypress.io/screenshot
|
// https://on.cypress.io/screenshot
|
||||||
cy.screenshot('my-image')
|
cy.screenshot("my-image");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
|
it("Cypress.Screenshot.defaults() - change default config of screenshots", function () {
|
||||||
Cypress.Screenshot.defaults({
|
Cypress.Screenshot.defaults({
|
||||||
blackout: ['.foo'],
|
blackout: [".foo"],
|
||||||
capture: 'viewport',
|
capture: "viewport",
|
||||||
clip: {x: 0, y: 0, width: 200, height: 200},
|
clip: { x: 0, y: 0, width: 200, height: 200 },
|
||||||
scale: false,
|
scale: false,
|
||||||
disableTimersAndAnimations: true,
|
disableTimersAndAnimations: true,
|
||||||
screenshotOnRunFailure: true,
|
screenshotOnRunFailure: true,
|
||||||
onBeforeScreenshot() {
|
onBeforeScreenshot() {},
|
||||||
},
|
onAfterScreenshot() {}
|
||||||
onAfterScreenshot() {
|
});
|
||||||
},
|
});
|
||||||
})
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.wrap() - wrap an object', () => {
|
it("cy.wrap() - wrap an object", () => {
|
||||||
// https://on.cypress.io/wrap
|
// https://on.cypress.io/wrap
|
||||||
cy.wrap({foo: 'bar'})
|
cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
|
||||||
.should('have.property', 'foo')
|
});
|
||||||
.and('include', 'bar')
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Navigation', () => {
|
context("Navigation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io')
|
cy.visit("https://example.cypress.io");
|
||||||
cy.get('.navbar-nav').contains('Commands').click()
|
cy.get(".navbar-nav").contains("Commands").click();
|
||||||
cy.get('.dropdown-menu').contains('Navigation').click()
|
cy.get(".dropdown-menu").contains("Navigation").click();
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.go() - go back or forward in the browser\'s history', () => {
|
it("cy.go() - go back or forward in the browser's history", () => {
|
||||||
// https://on.cypress.io/go
|
// https://on.cypress.io/go
|
||||||
|
|
||||||
cy.location('pathname').should('include', 'navigation')
|
cy.location("pathname").should("include", "navigation");
|
||||||
|
|
||||||
cy.go('back')
|
cy.go("back");
|
||||||
cy.location('pathname').should('not.include', 'navigation')
|
cy.location("pathname").should("not.include", "navigation");
|
||||||
|
|
||||||
cy.go('forward')
|
cy.go("forward");
|
||||||
cy.location('pathname').should('include', 'navigation')
|
cy.location("pathname").should("include", "navigation");
|
||||||
|
|
||||||
// clicking back
|
// clicking back
|
||||||
cy.go(-1)
|
cy.go(-1);
|
||||||
cy.location('pathname').should('not.include', 'navigation')
|
cy.location("pathname").should("not.include", "navigation");
|
||||||
|
|
||||||
// clicking forward
|
// clicking forward
|
||||||
cy.go(1)
|
cy.go(1);
|
||||||
cy.location('pathname').should('include', 'navigation')
|
cy.location("pathname").should("include", "navigation");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.reload() - reload the page', () => {
|
it("cy.reload() - reload the page", () => {
|
||||||
// https://on.cypress.io/reload
|
// https://on.cypress.io/reload
|
||||||
cy.reload()
|
cy.reload();
|
||||||
|
|
||||||
// reload the page without using the cache
|
// reload the page without using the cache
|
||||||
cy.reload(true)
|
cy.reload(true);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.visit() - visit a remote url', () => {
|
it("cy.visit() - visit a remote url", () => {
|
||||||
// https://on.cypress.io/visit
|
// https://on.cypress.io/visit
|
||||||
|
|
||||||
// Visit any sub-domain of your current domain
|
// Visit any sub-domain of your current domain
|
||||||
|
|
||||||
// Pass options to the visit
|
// Pass options to the visit
|
||||||
cy.visit('https://example.cypress.io/commands/navigation', {
|
cy.visit("https://example.cypress.io/commands/navigation", {
|
||||||
timeout: 50000, // increase total time for the visit to resolve
|
timeout: 50000, // increase total time for the visit to resolve
|
||||||
onBeforeLoad(contentWindow) {
|
onBeforeLoad(contentWindow) {
|
||||||
// contentWindow is the remote page's window object
|
// contentWindow is the remote page's window object
|
||||||
expect(typeof contentWindow === 'object').to.be.true
|
expect(typeof contentWindow === "object").to.be.true;
|
||||||
},
|
},
|
||||||
onLoad(contentWindow) {
|
onLoad(contentWindow) {
|
||||||
// contentWindow is the remote page's window object
|
// contentWindow is the remote page's window object
|
||||||
expect(typeof contentWindow === 'object').to.be.true
|
expect(typeof contentWindow === "object").to.be.true;
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,163 +1,165 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Network Requests', () => {
|
context("Network Requests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/network-requests')
|
cy.visit("https://example.cypress.io/commands/network-requests");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manage HTTP requests in your app
|
||||||
|
|
||||||
|
it("cy.request() - make an XHR request", () => {
|
||||||
|
// https://on.cypress.io/request
|
||||||
|
cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
// the server sometimes gets an extra comment posted from another machine
|
||||||
|
// which gets returned as 1 extra object
|
||||||
|
expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
|
||||||
|
expect(response).to.have.property("headers");
|
||||||
|
expect(response).to.have.property("duration");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cy.request() - verify response using BDD syntax", () => {
|
||||||
|
cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
|
||||||
|
// https://on.cypress.io/assertions
|
||||||
|
expect(response).property("status").to.equal(200);
|
||||||
|
expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
|
||||||
|
expect(response).to.include.keys("headers", "duration");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cy.request() with query parameters", () => {
|
||||||
|
// will execute request
|
||||||
|
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
||||||
|
cy.request({
|
||||||
|
url: "https://jsonplaceholder.cypress.io/comments",
|
||||||
|
qs: {
|
||||||
|
postId: 1,
|
||||||
|
id: 3
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.its("body")
|
||||||
|
.should("be.an", "array")
|
||||||
|
.and("have.length", 1)
|
||||||
|
.its("0") // yields first element of the array
|
||||||
|
.should("contain", {
|
||||||
|
postId: 1,
|
||||||
|
id: 3
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Manage HTTP requests in your app
|
it("cy.request() - pass result to the second request", () => {
|
||||||
|
// first, let's find out the userId of the first user we have
|
||||||
|
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
||||||
|
.its("body") // yields the response object
|
||||||
|
.its("0") // yields the first element of the returned list
|
||||||
|
// the above two commands its('body').its('0')
|
||||||
|
// can be written as its('body.0')
|
||||||
|
// if you do not care about TypeScript checks
|
||||||
|
.then((user) => {
|
||||||
|
expect(user).property("id").to.be.a("number");
|
||||||
|
// make a new post on behalf of the user
|
||||||
|
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
||||||
|
userId: user.id,
|
||||||
|
title: "Cypress Test Runner",
|
||||||
|
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// note that the value here is the returned value of the 2nd request
|
||||||
|
// which is the new post object
|
||||||
|
.then((response) => {
|
||||||
|
expect(response).property("status").to.equal(201); // new entity created
|
||||||
|
expect(response).property("body").to.contain({
|
||||||
|
title: "Cypress Test Runner"
|
||||||
|
});
|
||||||
|
|
||||||
it('cy.request() - make an XHR request', () => {
|
// we don't know the exact post id - only that it will be > 100
|
||||||
// https://on.cypress.io/request
|
// since JSONPlaceholder has built-in 100 posts
|
||||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
|
||||||
.should((response) => {
|
|
||||||
expect(response.status).to.eq(200)
|
|
||||||
// the server sometimes gets an extra comment posted from another machine
|
|
||||||
// which gets returned as 1 extra object
|
|
||||||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
|
|
||||||
expect(response).to.have.property('headers')
|
|
||||||
expect(response).to.have.property('duration')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() - verify response using BDD syntax', () => {
|
// we don't know the user id here - since it was in above closure
|
||||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
// so in this test just confirm that the property is there
|
||||||
.then((response) => {
|
expect(response.body).property("userId").to.be.a("number");
|
||||||
// https://on.cypress.io/assertions
|
});
|
||||||
expect(response).property('status').to.equal(200)
|
});
|
||||||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
|
|
||||||
expect(response).to.include.keys('headers', 'duration')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() with query parameters', () => {
|
it("cy.request() - save response in the shared test context", () => {
|
||||||
// will execute request
|
// https://on.cypress.io/variables-and-aliases
|
||||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
||||||
cy.request({
|
.its("body")
|
||||||
url: 'https://jsonplaceholder.cypress.io/comments',
|
.its("0") // yields the first element of the returned list
|
||||||
qs: {
|
.as("user") // saves the object in the test context
|
||||||
postId: 1,
|
.then(function () {
|
||||||
id: 3,
|
// NOTE 👀
|
||||||
},
|
// By the time this callback runs the "as('user')" command
|
||||||
|
// has saved the user object in the test context.
|
||||||
|
// To access the test context we need to use
|
||||||
|
// the "function () { ... }" callback form,
|
||||||
|
// otherwise "this" points at a wrong or undefined object!
|
||||||
|
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
||||||
|
userId: this.user.id,
|
||||||
|
title: "Cypress Test Runner",
|
||||||
|
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
||||||
})
|
})
|
||||||
.its('body')
|
.its("body")
|
||||||
.should('be.an', 'array')
|
.as("post"); // save the new post from the response
|
||||||
.and('have.length', 1)
|
})
|
||||||
.its('0') // yields first element of the array
|
.then(function () {
|
||||||
.should('contain', {
|
// When this callback runs, both "cy.request" API commands have finished
|
||||||
postId: 1,
|
// and the test context has "user" and "post" objects set.
|
||||||
id: 3,
|
// Let's verify them.
|
||||||
})
|
expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id);
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('cy.request() - pass result to the second request', () => {
|
it("cy.intercept() - route responses to matching requests", () => {
|
||||||
// first, let's find out the userId of the first user we have
|
// https://on.cypress.io/intercept
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
|
||||||
.its('body') // yields the response object
|
|
||||||
.its('0') // yields the first element of the returned list
|
|
||||||
// the above two commands its('body').its('0')
|
|
||||||
// can be written as its('body.0')
|
|
||||||
// if you do not care about TypeScript checks
|
|
||||||
.then((user) => {
|
|
||||||
expect(user).property('id').to.be.a('number')
|
|
||||||
// make a new post on behalf of the user
|
|
||||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
|
||||||
userId: user.id,
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// note that the value here is the returned value of the 2nd request
|
|
||||||
// which is the new post object
|
|
||||||
.then((response) => {
|
|
||||||
expect(response).property('status').to.equal(201) // new entity created
|
|
||||||
expect(response).property('body').to.contain({
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
})
|
|
||||||
|
|
||||||
// we don't know the exact post id - only that it will be > 100
|
let message = "whoa, this comment does not exist";
|
||||||
// since JSONPlaceholder has built-in 100 posts
|
|
||||||
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
|
// Listen to GET to comments/1
|
||||||
// so in this test just confirm that the property is there
|
cy.intercept("GET", "**/comments/*").as("getComment");
|
||||||
expect(response.body).property('userId').to.be.a('number')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() - save response in the shared test context', () => {
|
// we have code that gets a comment when
|
||||||
// https://on.cypress.io/variables-and-aliases
|
// the button is clicked in scripts.js
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
cy.get(".network-btn").click();
|
||||||
.its('body').its('0') // yields the first element of the returned list
|
|
||||||
.as('user') // saves the object in the test context
|
|
||||||
.then(function () {
|
|
||||||
// NOTE 👀
|
|
||||||
// By the time this callback runs the "as('user')" command
|
|
||||||
// has saved the user object in the test context.
|
|
||||||
// To access the test context we need to use
|
|
||||||
// the "function () { ... }" callback form,
|
|
||||||
// otherwise "this" points at a wrong or undefined object!
|
|
||||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
|
||||||
userId: this.user.id,
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
|
||||||
})
|
|
||||||
.its('body').as('post') // save the new post from the response
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
// When this callback runs, both "cy.request" API commands have finished
|
|
||||||
// and the test context has "user" and "post" objects set.
|
|
||||||
// Let's verify them.
|
|
||||||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.intercept() - route responses to matching requests', () => {
|
// https://on.cypress.io/wait
|
||||||
// https://on.cypress.io/intercept
|
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
||||||
|
|
||||||
let message = 'whoa, this comment does not exist'
|
// Listen to POST to comments
|
||||||
|
cy.intercept("POST", "**/comments").as("postComment");
|
||||||
|
|
||||||
// Listen to GET to comments/1
|
// we have code that posts a comment when
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
// the button is clicked in scripts.js
|
||||||
|
cy.get(".network-post").click();
|
||||||
|
cy.wait("@postComment").should(({ request, response }) => {
|
||||||
|
expect(request.body).to.include("email");
|
||||||
|
expect(request.headers).to.have.property("content-type");
|
||||||
|
expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
|
||||||
|
});
|
||||||
|
|
||||||
// we have code that gets a comment when
|
// Stub a response to PUT comments/ ****
|
||||||
// the button is clicked in scripts.js
|
cy.intercept(
|
||||||
cy.get('.network-btn').click()
|
{
|
||||||
|
method: "PUT",
|
||||||
|
url: "**/comments/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
statusCode: 404,
|
||||||
|
body: { error: message },
|
||||||
|
headers: { "access-control-allow-origin": "*" },
|
||||||
|
delayMs: 500
|
||||||
|
}
|
||||||
|
).as("putComment");
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
// we have code that puts a comment when
|
||||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
// the button is clicked in scripts.js
|
||||||
|
cy.get(".network-put").click();
|
||||||
|
|
||||||
// Listen to POST to comments
|
cy.wait("@putComment");
|
||||||
cy.intercept('POST', '**/comments').as('postComment')
|
|
||||||
|
|
||||||
// we have code that posts a comment when
|
// our 404 statusCode logic in scripts.js executed
|
||||||
// the button is clicked in scripts.js
|
cy.get(".network-put-comment").should("contain", message);
|
||||||
cy.get('.network-post').click()
|
});
|
||||||
cy.wait('@postComment').should(({request, response}) => {
|
});
|
||||||
expect(request.body).to.include('email')
|
|
||||||
expect(request.headers).to.have.property('content-type')
|
|
||||||
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stub a response to PUT comments/ ****
|
|
||||||
cy.intercept({
|
|
||||||
method: 'PUT',
|
|
||||||
url: '**/comments/*',
|
|
||||||
}, {
|
|
||||||
statusCode: 404,
|
|
||||||
body: {error: message},
|
|
||||||
headers: {'access-control-allow-origin': '*'},
|
|
||||||
delayMs: 500,
|
|
||||||
}).as('putComment')
|
|
||||||
|
|
||||||
// we have code that puts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-put').click()
|
|
||||||
|
|
||||||
cy.wait('@putComment')
|
|
||||||
|
|
||||||
// our 404 statusCode logic in scripts.js executed
|
|
||||||
cy.get('.network-put-comment').should('contain', message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,114 +1,100 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Querying', () => {
|
context("Querying", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/querying')
|
cy.visit("https://example.cypress.io/commands/querying");
|
||||||
})
|
});
|
||||||
|
|
||||||
// The most commonly used query is 'cy.get()', you can
|
// The most commonly used query is 'cy.get()', you can
|
||||||
// think of this like the '$' in jQuery
|
// think of this like the '$' in jQuery
|
||||||
|
|
||||||
it('cy.get() - query DOM elements', () => {
|
it("cy.get() - query DOM elements", () => {
|
||||||
// https://on.cypress.io/get
|
// https://on.cypress.io/get
|
||||||
|
|
||||||
cy.get('#query-btn').should('contain', 'Button')
|
cy.get("#query-btn").should("contain", "Button");
|
||||||
|
|
||||||
cy.get('.query-btn').should('contain', 'Button')
|
cy.get(".query-btn").should("contain", "Button");
|
||||||
|
|
||||||
cy.get('#querying .well>button:first').should('contain', 'Button')
|
cy.get("#querying .well>button:first").should("contain", "Button");
|
||||||
// ↲
|
// ↲
|
||||||
// Use CSS selectors just like jQuery
|
// Use CSS selectors just like jQuery
|
||||||
|
|
||||||
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
|
cy.get('[data-test-id="test-example"]').should("have.class", "example");
|
||||||
|
|
||||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
// 'cy.get()' yields jQuery object, you can get its attribute
|
||||||
// by invoking `.attr()` method
|
// by invoking `.attr()` method
|
||||||
cy.get('[data-test-id="test-example"]')
|
cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
|
||||||
.invoke('attr', 'data-test-id')
|
|
||||||
.should('equal', 'test-example')
|
|
||||||
|
|
||||||
// or you can get element's CSS property
|
// or you can get element's CSS property
|
||||||
cy.get('[data-test-id="test-example"]')
|
cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
|
||||||
.invoke('css', 'position')
|
|
||||||
.should('equal', 'static')
|
|
||||||
|
|
||||||
// or use assertions directly during 'cy.get()'
|
// or use assertions directly during 'cy.get()'
|
||||||
// https://on.cypress.io/assertions
|
// https://on.cypress.io/assertions
|
||||||
cy.get('[data-test-id="test-example"]')
|
cy.get('[data-test-id="test-example"]')
|
||||||
.should('have.attr', 'data-test-id', 'test-example')
|
.should("have.attr", "data-test-id", "test-example")
|
||||||
.and('have.css', 'position', 'static')
|
.and("have.css", "position", "static");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.contains() - query DOM elements with matching content', () => {
|
it("cy.contains() - query DOM elements with matching content", () => {
|
||||||
// https://on.cypress.io/contains
|
// https://on.cypress.io/contains
|
||||||
cy.get('.query-list')
|
cy.get(".query-list").contains("bananas").should("have.class", "third");
|
||||||
.contains('bananas')
|
|
||||||
.should('have.class', 'third')
|
|
||||||
|
|
||||||
// we can pass a regexp to `.contains()`
|
// we can pass a regexp to `.contains()`
|
||||||
cy.get('.query-list')
|
cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
|
||||||
.contains(/^b\w+/)
|
|
||||||
.should('have.class', 'third')
|
|
||||||
|
|
||||||
cy.get('.query-list')
|
cy.get(".query-list").contains("apples").should("have.class", "first");
|
||||||
.contains('apples')
|
|
||||||
.should('have.class', 'first')
|
|
||||||
|
|
||||||
// passing a selector to contains will
|
// passing a selector to contains will
|
||||||
// yield the selector containing the text
|
// yield the selector containing the text
|
||||||
cy.get('#querying')
|
cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
|
||||||
.contains('ul', 'oranges')
|
|
||||||
.should('have.class', 'query-list')
|
|
||||||
|
|
||||||
cy.get('.query-button')
|
cy.get(".query-button").contains("Save Form").should("have.class", "btn");
|
||||||
.contains('Save Form')
|
});
|
||||||
.should('have.class', 'btn')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.within() - query DOM elements within a specific element', () => {
|
it(".within() - query DOM elements within a specific element", () => {
|
||||||
// https://on.cypress.io/within
|
// https://on.cypress.io/within
|
||||||
cy.get('.query-form').within(() => {
|
cy.get(".query-form").within(() => {
|
||||||
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
|
cy.get("input:first").should("have.attr", "placeholder", "Email");
|
||||||
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
|
cy.get("input:last").should("have.attr", "placeholder", "Password");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.root() - query the root DOM element', () => {
|
it("cy.root() - query the root DOM element", () => {
|
||||||
// https://on.cypress.io/root
|
// https://on.cypress.io/root
|
||||||
|
|
||||||
// By default, root is the document
|
// By default, root is the document
|
||||||
cy.root().should('match', 'html')
|
cy.root().should("match", "html");
|
||||||
|
|
||||||
cy.get('.query-ul').within(() => {
|
cy.get(".query-ul").within(() => {
|
||||||
// In this within, the root is now the ul DOM element
|
// In this within, the root is now the ul DOM element
|
||||||
cy.root().should('have.class', 'query-ul')
|
cy.root().should("have.class", "query-ul");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('best practices - selecting elements', () => {
|
it("best practices - selecting elements", () => {
|
||||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
// https://on.cypress.io/best-practices#Selecting-Elements
|
||||||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
|
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
|
||||||
// Worst - too generic, no context
|
// Worst - too generic, no context
|
||||||
cy.get('button').click()
|
cy.get("button").click();
|
||||||
|
|
||||||
// Bad. Coupled to styling. Highly subject to change.
|
// Bad. Coupled to styling. Highly subject to change.
|
||||||
cy.get('.btn.btn-large').click()
|
cy.get(".btn.btn-large").click();
|
||||||
|
|
||||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
// Average. Coupled to the `name` attribute which has HTML semantics.
|
||||||
cy.get('[name=submission]').click()
|
cy.get("[name=submission]").click();
|
||||||
|
|
||||||
// Better. But still coupled to styling or JS event listeners.
|
// Better. But still coupled to styling or JS event listeners.
|
||||||
cy.get('#main').click()
|
cy.get("#main").click();
|
||||||
|
|
||||||
// Slightly better. Uses an ID but also ensures the element
|
// Slightly better. Uses an ID but also ensures the element
|
||||||
// has an ARIA role attribute
|
// has an ARIA role attribute
|
||||||
cy.get('#main[role=button]').click()
|
cy.get("#main[role=button]").click();
|
||||||
|
|
||||||
// Much better. But still coupled to text content that may change.
|
// Much better. But still coupled to text content that may change.
|
||||||
cy.contains('Submit').click()
|
cy.contains("Submit").click();
|
||||||
|
|
||||||
// Best. Insulated from all changes.
|
// Best. Insulated from all changes.
|
||||||
cy.get('[data-cy=submit]').click()
|
cy.get("[data-cy=submit]").click();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -2,205 +2,202 @@
|
|||||||
// remove no check once Cypress.sinon is typed
|
// remove no check once Cypress.sinon is typed
|
||||||
// https://github.com/cypress-io/cypress/issues/6720
|
// https://github.com/cypress-io/cypress/issues/6720
|
||||||
|
|
||||||
context('Spies, Stubs, and Clock', () => {
|
context("Spies, Stubs, and Clock", () => {
|
||||||
it('cy.spy() - wrap a method in a spy', () => {
|
it("cy.spy() - wrap a method in a spy", () => {
|
||||||
// https://on.cypress.io/spy
|
// https://on.cypress.io/spy
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
foo() {
|
foo() {}
|
||||||
},
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const spy = cy.spy(obj, 'foo').as('anyArgs')
|
const spy = cy.spy(obj, "foo").as("anyArgs");
|
||||||
|
|
||||||
obj.foo()
|
obj.foo();
|
||||||
|
|
||||||
expect(spy).to.be.called
|
expect(spy).to.be.called;
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.spy() retries until assertions pass', () => {
|
it("cy.spy() retries until assertions pass", () => {
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
/**
|
/**
|
||||||
* Prints the argument passed
|
* Prints the argument passed
|
||||||
* @param x {any}
|
* @param x {any}
|
||||||
*/
|
*/
|
||||||
foo(x) {
|
foo(x) {
|
||||||
console.log('obj.foo called with', x)
|
console.log("obj.foo called with", x);
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
cy.spy(obj, 'foo').as('foo')
|
cy.spy(obj, "foo").as("foo");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
obj.foo('first')
|
obj.foo("first");
|
||||||
}, 500)
|
}, 500);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
obj.foo('second')
|
obj.foo("second");
|
||||||
}, 2500)
|
}, 2500);
|
||||||
|
|
||||||
cy.get('@foo').should('have.been.calledTwice')
|
cy.get("@foo").should("have.been.calledTwice");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.stub() - create a stub and/or replace a function with stub', () => {
|
it("cy.stub() - create a stub and/or replace a function with stub", () => {
|
||||||
// https://on.cypress.io/stub
|
// https://on.cypress.io/stub
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
/**
|
/**
|
||||||
* prints both arguments to the console
|
* prints both arguments to the console
|
||||||
* @param a {string}
|
* @param a {string}
|
||||||
* @param b {string}
|
* @param b {string}
|
||||||
*/
|
*/
|
||||||
foo(a, b) {
|
foo(a, b) {
|
||||||
console.log('a', a, 'b', b)
|
console.log("a", a, "b", b);
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const stub = cy.stub(obj, 'foo').as('foo')
|
const stub = cy.stub(obj, "foo").as("foo");
|
||||||
|
|
||||||
obj.foo('foo', 'bar')
|
obj.foo("foo", "bar");
|
||||||
|
|
||||||
expect(stub).to.be.called
|
expect(stub).to.be.called;
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.clock() - control time in the browser', () => {
|
it("cy.clock() - control time in the browser", () => {
|
||||||
// https://on.cypress.io/clock
|
// https://on.cypress.io/clock
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
// create the date in UTC so its always the same
|
||||||
// no matter what local timezone the browser is running in
|
// no matter what local timezone the browser is running in
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
||||||
|
|
||||||
cy.clock(now)
|
cy.clock(now);
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
||||||
cy.get('#clock-div').click()
|
cy.get("#clock-div").click().should("have.text", "1489449600");
|
||||||
.should('have.text', '1489449600')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.tick() - move time in the browser', () => {
|
it("cy.tick() - move time in the browser", () => {
|
||||||
// https://on.cypress.io/tick
|
// https://on.cypress.io/tick
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
// create the date in UTC so its always the same
|
||||||
// no matter what local timezone the browser is running in
|
// no matter what local timezone the browser is running in
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
||||||
|
|
||||||
cy.clock(now)
|
cy.clock(now);
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
||||||
cy.get('#tick-div').click()
|
cy.get("#tick-div").click().should("have.text", "1489449600");
|
||||||
.should('have.text', '1489449600')
|
|
||||||
|
|
||||||
cy.tick(10000) // 10 seconds passed
|
cy.tick(10000); // 10 seconds passed
|
||||||
cy.get('#tick-div').click()
|
cy.get("#tick-div").click().should("have.text", "1489449610");
|
||||||
.should('have.text', '1489449610')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.stub() matches depending on arguments', () => {
|
it("cy.stub() matches depending on arguments", () => {
|
||||||
// see all possible matchers at
|
// see all possible matchers at
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
// https://sinonjs.org/releases/latest/matchers/
|
||||||
const greeter = {
|
const greeter = {
|
||||||
/**
|
/**
|
||||||
* Greets a person
|
* Greets a person
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
*/
|
*/
|
||||||
greet(name) {
|
greet(name) {
|
||||||
return `Hello, ${name}!`
|
return `Hello, ${name}!`;
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
cy.stub(greeter, 'greet')
|
cy.stub(greeter, "greet")
|
||||||
.callThrough() // if you want non-matched calls to call the real method
|
.callThrough() // if you want non-matched calls to call the real method
|
||||||
.withArgs(Cypress.sinon.match.string).returns('Hi')
|
.withArgs(Cypress.sinon.match.string)
|
||||||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
|
.returns("Hi")
|
||||||
|
.withArgs(Cypress.sinon.match.number)
|
||||||
|
.throws(new Error("Invalid name"));
|
||||||
|
|
||||||
expect(greeter.greet('World')).to.equal('Hi')
|
expect(greeter.greet("World")).to.equal("Hi");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(() => greeter.greet(42)).to.throw('Invalid name')
|
expect(() => greeter.greet(42)).to.throw("Invalid name");
|
||||||
expect(greeter.greet).to.have.been.calledTwice
|
expect(greeter.greet).to.have.been.calledTwice;
|
||||||
|
|
||||||
// non-matched calls goes the actual method
|
// non-matched calls goes the actual method
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(greeter.greet()).to.equal('Hello, undefined!')
|
expect(greeter.greet()).to.equal("Hello, undefined!");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('matches call arguments using Sinon matchers', () => {
|
it("matches call arguments using Sinon matchers", () => {
|
||||||
// see all possible matchers at
|
// see all possible matchers at
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
// https://sinonjs.org/releases/latest/matchers/
|
||||||
const calculator = {
|
const calculator = {
|
||||||
/**
|
/**
|
||||||
* returns the sum of two arguments
|
* returns the sum of two arguments
|
||||||
* @param a {number}
|
* @param a {number}
|
||||||
* @param b {number}
|
* @param b {number}
|
||||||
*/
|
*/
|
||||||
add(a, b) {
|
add(a, b) {
|
||||||
return a + b
|
return a + b;
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const spy = cy.spy(calculator, 'add').as('add')
|
const spy = cy.spy(calculator, "add").as("add");
|
||||||
|
|
||||||
expect(calculator.add(2, 3)).to.equal(5)
|
expect(calculator.add(2, 3)).to.equal(5);
|
||||||
|
|
||||||
// if we want to assert the exact values used during the call
|
// if we want to assert the exact values used during the call
|
||||||
expect(spy).to.be.calledWith(2, 3)
|
expect(spy).to.be.calledWith(2, 3);
|
||||||
|
|
||||||
// let's confirm "add" method was called with two numbers
|
// let's confirm "add" method was called with two numbers
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
|
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
|
||||||
|
|
||||||
// alternatively, provide the value to match
|
// alternatively, provide the value to match
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
|
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
|
||||||
|
|
||||||
// match any value
|
// match any value
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
|
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
|
||||||
|
|
||||||
// match any value from a list
|
// match any value from a list
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
|
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given number is event
|
* Returns true if the given number is event
|
||||||
* @param {number} x
|
* @param {number} x
|
||||||
*/
|
*/
|
||||||
const isEven = (x) => x % 2 === 0
|
const isEven = (x) => x % 2 === 0;
|
||||||
|
|
||||||
// expect the value to pass a custom predicate function
|
// expect the value to pass a custom predicate function
|
||||||
// the second argument to "sinon.match(predicate, message)" is
|
// the second argument to "sinon.match(predicate, message)" is
|
||||||
// shown if the predicate does not pass and assertion fails
|
// shown if the predicate does not pass and assertion fails
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
|
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a function that checks if a given number is larger than the limit
|
* Returns a function that checks if a given number is larger than the limit
|
||||||
* @param {number} limit
|
* @param {number} limit
|
||||||
* @returns {(x: number) => boolean}
|
* @returns {(x: number) => boolean}
|
||||||
*/
|
*/
|
||||||
const isGreaterThan = (limit) => (x) => x > limit
|
const isGreaterThan = (limit) => (x) => x > limit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a function that checks if a given number is less than the limit
|
* Returns a function that checks if a given number is less than the limit
|
||||||
* @param {number} limit
|
* @param {number} limit
|
||||||
* @returns {(x: number) => boolean}
|
* @returns {(x: number) => boolean}
|
||||||
*/
|
*/
|
||||||
const isLessThan = (limit) => (x) => x < limit
|
const isLessThan = (limit) => (x) => x < limit;
|
||||||
|
|
||||||
// you can combine several matchers using "and", "or"
|
// you can combine several matchers using "and", "or"
|
||||||
expect(spy).to.be.calledWith(
|
expect(spy).to.be.calledWith(
|
||||||
Cypress.sinon.match.number,
|
Cypress.sinon.match.number,
|
||||||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
|
Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
|
||||||
)
|
);
|
||||||
|
|
||||||
expect(spy).to.be.calledWith(
|
expect(spy).to.be.calledWith(
|
||||||
Cypress.sinon.match.number,
|
Cypress.sinon.match.number,
|
||||||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
|
Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
|
||||||
)
|
);
|
||||||
|
|
||||||
// matchers can be used from BDD assertions
|
// matchers can be used from BDD assertions
|
||||||
cy.get('@add').should('have.been.calledWith',
|
cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
|
||||||
Cypress.sinon.match.number, Cypress.sinon.match(3))
|
|
||||||
|
|
||||||
// you can alias matchers for shorter test code
|
// you can alias matchers for shorter test code
|
||||||
const {match: M} = Cypress.sinon
|
const { match: M } = Cypress.sinon;
|
||||||
|
|
||||||
cy.get('@add').should('have.been.calledWith', M.number, M(3))
|
cy.get("@add").should("have.been.calledWith", M.number, M(3));
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,121 +1,97 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Traversal', () => {
|
context("Traversal", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/traversal')
|
cy.visit("https://example.cypress.io/commands/traversal");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('.children() - get child DOM elements', () => {
|
it(".children() - get child DOM elements", () => {
|
||||||
// https://on.cypress.io/children
|
// https://on.cypress.io/children
|
||||||
cy.get('.traversal-breadcrumb')
|
cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
|
||||||
.children('.active')
|
});
|
||||||
.should('contain', 'Data')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.closest() - get closest ancestor DOM element', () => {
|
it(".closest() - get closest ancestor DOM element", () => {
|
||||||
// https://on.cypress.io/closest
|
// https://on.cypress.io/closest
|
||||||
cy.get('.traversal-badge')
|
cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
|
||||||
.closest('ul')
|
});
|
||||||
.should('have.class', 'list-group')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.eq() - get a DOM element at a specific index', () => {
|
it(".eq() - get a DOM element at a specific index", () => {
|
||||||
// https://on.cypress.io/eq
|
// https://on.cypress.io/eq
|
||||||
cy.get('.traversal-list>li')
|
cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
|
||||||
.eq(1).should('contain', 'siamese')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.filter() - get DOM elements that match the selector', () => {
|
it(".filter() - get DOM elements that match the selector", () => {
|
||||||
// https://on.cypress.io/filter
|
// https://on.cypress.io/filter
|
||||||
cy.get('.traversal-nav>li')
|
cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
|
||||||
.filter('.active').should('contain', 'About')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.find() - get descendant DOM elements of the selector', () => {
|
it(".find() - get descendant DOM elements of the selector", () => {
|
||||||
// https://on.cypress.io/find
|
// https://on.cypress.io/find
|
||||||
cy.get('.traversal-pagination')
|
cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
|
||||||
.find('li').find('a')
|
});
|
||||||
.should('have.length', 7)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.first() - get first DOM element', () => {
|
it(".first() - get first DOM element", () => {
|
||||||
// https://on.cypress.io/first
|
// https://on.cypress.io/first
|
||||||
cy.get('.traversal-table td')
|
cy.get(".traversal-table td").first().should("contain", "1");
|
||||||
.first().should('contain', '1')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.last() - get last DOM element', () => {
|
it(".last() - get last DOM element", () => {
|
||||||
// https://on.cypress.io/last
|
// https://on.cypress.io/last
|
||||||
cy.get('.traversal-buttons .btn')
|
cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
|
||||||
.last().should('contain', 'Submit')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.next() - get next sibling DOM element', () => {
|
it(".next() - get next sibling DOM element", () => {
|
||||||
// https://on.cypress.io/next
|
// https://on.cypress.io/next
|
||||||
cy.get('.traversal-ul')
|
cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
|
||||||
.contains('apples').next().should('contain', 'oranges')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.nextAll() - get all next sibling DOM elements', () => {
|
it(".nextAll() - get all next sibling DOM elements", () => {
|
||||||
// https://on.cypress.io/nextall
|
// https://on.cypress.io/nextall
|
||||||
cy.get('.traversal-next-all')
|
cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
|
||||||
.contains('oranges')
|
});
|
||||||
.nextAll().should('have.length', 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.nextUntil() - get next sibling DOM elements until next el', () => {
|
it(".nextUntil() - get next sibling DOM elements until next el", () => {
|
||||||
// https://on.cypress.io/nextuntil
|
// https://on.cypress.io/nextuntil
|
||||||
cy.get('#veggies')
|
cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
|
||||||
.nextUntil('#nuts').should('have.length', 3)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.not() - remove DOM elements from set of DOM elements', () => {
|
it(".not() - remove DOM elements from set of DOM elements", () => {
|
||||||
// https://on.cypress.io/not
|
// https://on.cypress.io/not
|
||||||
cy.get('.traversal-disabled .btn')
|
cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
|
||||||
.not('[disabled]').should('not.contain', 'Disabled')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.parent() - get parent DOM element from DOM elements', () => {
|
it(".parent() - get parent DOM element from DOM elements", () => {
|
||||||
// https://on.cypress.io/parent
|
// https://on.cypress.io/parent
|
||||||
cy.get('.traversal-mark')
|
cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
|
||||||
.parent().should('contain', 'Morbi leo risus')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.parents() - get parent DOM elements from DOM elements', () => {
|
it(".parents() - get parent DOM elements from DOM elements", () => {
|
||||||
// https://on.cypress.io/parents
|
// https://on.cypress.io/parents
|
||||||
cy.get('.traversal-cite')
|
cy.get(".traversal-cite").parents().should("match", "blockquote");
|
||||||
.parents().should('match', 'blockquote')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
|
it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
|
||||||
// https://on.cypress.io/parentsuntil
|
// https://on.cypress.io/parentsuntil
|
||||||
cy.get('.clothes-nav')
|
cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
|
||||||
.find('.active')
|
});
|
||||||
.parentsUntil('.clothes-nav')
|
|
||||||
.should('have.length', 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.prev() - get previous sibling DOM element', () => {
|
it(".prev() - get previous sibling DOM element", () => {
|
||||||
// https://on.cypress.io/prev
|
// https://on.cypress.io/prev
|
||||||
cy.get('.birds').find('.active')
|
cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
|
||||||
.prev().should('contain', 'Lorikeets')
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.prevAll() - get all previous sibling DOM elements', () => {
|
it(".prevAll() - get all previous sibling DOM elements", () => {
|
||||||
// https://on.cypress.io/prevall
|
// https://on.cypress.io/prevall
|
||||||
cy.get('.fruits-list').find('.third')
|
cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
|
||||||
.prevAll().should('have.length', 2)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
|
it(".prevUntil() - get all previous sibling DOM elements until el", () => {
|
||||||
// https://on.cypress.io/prevuntil
|
// https://on.cypress.io/prevuntil
|
||||||
cy.get('.foods-list').find('#nuts')
|
cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
|
||||||
.prevUntil('#veggies').should('have.length', 3)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('.siblings() - get all sibling DOM elements', () => {
|
it(".siblings() - get all sibling DOM elements", () => {
|
||||||
// https://on.cypress.io/siblings
|
// https://on.cypress.io/siblings
|
||||||
cy.get('.traversal-pills .active')
|
cy.get(".traversal-pills .active").siblings().should("have.length", 2);
|
||||||
.siblings().should('have.length', 2)
|
});
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,110 +1,108 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Utilities', () => {
|
context("Utilities", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/utilities')
|
cy.visit("https://example.cypress.io/utilities");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Cypress._ - call a lodash method', () => {
|
it("Cypress._ - call a lodash method", () => {
|
||||||
// https://on.cypress.io/_
|
// https://on.cypress.io/_
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
||||||
.then((response) => {
|
let ids = Cypress._.chain(response.body).map("id").take(3).value();
|
||||||
let ids = Cypress._.chain(response.body).map('id').take(3).value()
|
|
||||||
|
|
||||||
expect(ids).to.deep.eq([1, 2, 3])
|
expect(ids).to.deep.eq([1, 2, 3]);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Cypress.$ - call a jQuery method', () => {
|
it("Cypress.$ - call a jQuery method", () => {
|
||||||
// https://on.cypress.io/$
|
// https://on.cypress.io/$
|
||||||
let $li = Cypress.$('.utility-jquery li:first')
|
let $li = Cypress.$(".utility-jquery li:first");
|
||||||
|
|
||||||
cy.wrap($li)
|
cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active");
|
||||||
.should('not.have.class', 'active')
|
});
|
||||||
.click()
|
|
||||||
.should('have.class', 'active')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
|
it("Cypress.Blob - blob utilities and base64 string conversion", () => {
|
||||||
// https://on.cypress.io/blob
|
// https://on.cypress.io/blob
|
||||||
cy.get('.utility-blob').then(($div) => {
|
cy.get(".utility-blob").then(($div) => {
|
||||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
||||||
// get the dataUrl string for the javascript-logo
|
// get the dataUrl string for the javascript-logo
|
||||||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
|
return Cypress.Blob.imgSrcToDataURL(
|
||||||
.then((dataUrl) => {
|
"https://example.cypress.io/assets/img/javascript-logo.png",
|
||||||
// create an <img> element and set its src to the dataUrl
|
undefined,
|
||||||
let img = Cypress.$('<img />', {src: dataUrl})
|
"anonymous"
|
||||||
|
).then((dataUrl) => {
|
||||||
|
// create an <img> element and set its src to the dataUrl
|
||||||
|
let img = Cypress.$("<img />", { src: dataUrl });
|
||||||
|
|
||||||
// need to explicitly return cy here since we are initially returning
|
// need to explicitly return cy here since we are initially returning
|
||||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
||||||
// append the image
|
// append the image
|
||||||
$div.append(img)
|
$div.append(img);
|
||||||
|
|
||||||
cy.get('.utility-blob img').click()
|
cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl);
|
||||||
.should('have.attr', 'src', dataUrl)
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.minimatch - test out glob patterns against strings', () => {
|
it("Cypress.minimatch - test out glob patterns against strings", () => {
|
||||||
// https://on.cypress.io/minimatch
|
// https://on.cypress.io/minimatch
|
||||||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
|
let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
|
||||||
matchBase: true,
|
matchBase: true
|
||||||
})
|
});
|
||||||
|
|
||||||
expect(matching, 'matching wildcard').to.be.true
|
expect(matching, "matching wildcard").to.be.true;
|
||||||
|
|
||||||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
|
matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
|
||||||
matchBase: true,
|
matchBase: true
|
||||||
})
|
});
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.false
|
expect(matching, "comments").to.be.false;
|
||||||
|
|
||||||
// ** matches against all downstream path segments
|
// ** matches against all downstream path segments
|
||||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
|
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
|
||||||
matchBase: true,
|
matchBase: true
|
||||||
})
|
});
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.true
|
expect(matching, "comments").to.be.true;
|
||||||
|
|
||||||
// whereas * matches only the next path segment
|
// whereas * matches only the next path segment
|
||||||
|
|
||||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
|
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
|
||||||
matchBase: false,
|
matchBase: false
|
||||||
})
|
});
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.false
|
expect(matching, "comments").to.be.false;
|
||||||
})
|
});
|
||||||
|
|
||||||
it('Cypress.Promise - instantiate a bluebird promise', () => {
|
it("Cypress.Promise - instantiate a bluebird promise", () => {
|
||||||
// https://on.cypress.io/promise
|
// https://on.cypress.io/promise
|
||||||
let waited = false
|
let waited = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Bluebird<string>
|
* @return Bluebird<string>
|
||||||
*/
|
*/
|
||||||
function waitOneSecond() {
|
function waitOneSecond() {
|
||||||
// return a promise that resolves after 1 second
|
// return a promise that resolves after 1 second
|
||||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
// @ts-ignore TS2351 (new Cypress.Promise)
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
return new Cypress.Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// set waited to true
|
// set waited to true
|
||||||
waited = true
|
waited = true;
|
||||||
|
|
||||||
// resolve with 'foo' string
|
// resolve with 'foo' string
|
||||||
resolve('foo')
|
resolve("foo");
|
||||||
}, 1000)
|
}, 1000);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.then(() => {
|
cy.then(() => {
|
||||||
// return a promise to cy.then() that
|
// return a promise to cy.then() that
|
||||||
// is awaited until it resolves
|
// is awaited until it resolves
|
||||||
// @ts-ignore TS7006
|
// @ts-ignore TS7006
|
||||||
return waitOneSecond().then((str) => {
|
return waitOneSecond().then((str) => {
|
||||||
expect(str).to.eq('foo')
|
expect(str).to.eq("foo");
|
||||||
expect(waited).to.be.true
|
expect(waited).to.be.true;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Viewport', () => {
|
context("Viewport", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/viewport')
|
cy.visit("https://example.cypress.io/commands/viewport");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.viewport() - set the viewport size and dimension', () => {
|
it("cy.viewport() - set the viewport size and dimension", () => {
|
||||||
// https://on.cypress.io/viewport
|
// https://on.cypress.io/viewport
|
||||||
|
|
||||||
cy.get('#navbar').should('be.visible')
|
cy.get("#navbar").should("be.visible");
|
||||||
cy.viewport(320, 480)
|
cy.viewport(320, 480);
|
||||||
|
|
||||||
// the navbar should have collapse since our screen is smaller
|
// the navbar should have collapse since our screen is smaller
|
||||||
cy.get('#navbar').should('not.be.visible')
|
cy.get("#navbar").should("not.be.visible");
|
||||||
cy.get('.navbar-toggle').should('be.visible').click()
|
cy.get(".navbar-toggle").should("be.visible").click();
|
||||||
cy.get('.nav').find('a').should('be.visible')
|
cy.get(".nav").find("a").should("be.visible");
|
||||||
|
|
||||||
// lets see what our app looks like on a super large screen
|
// lets see what our app looks like on a super large screen
|
||||||
cy.viewport(2999, 2999)
|
cy.viewport(2999, 2999);
|
||||||
|
|
||||||
// cy.viewport() accepts a set of preset sizes
|
// cy.viewport() accepts a set of preset sizes
|
||||||
// to easily set the screen to a device's width and height
|
// to easily set the screen to a device's width and height
|
||||||
|
|
||||||
// We added a cy.wait() between each viewport change so you can see
|
// We added a cy.wait() between each viewport change so you can see
|
||||||
// the change otherwise it is a little too fast to see :)
|
// the change otherwise it is a little too fast to see :)
|
||||||
|
|
||||||
cy.viewport('macbook-15')
|
cy.viewport("macbook-15");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('macbook-13')
|
cy.viewport("macbook-13");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('macbook-11')
|
cy.viewport("macbook-11");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('ipad-2')
|
cy.viewport("ipad-2");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('ipad-mini')
|
cy.viewport("ipad-mini");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-6+')
|
cy.viewport("iphone-6+");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-6')
|
cy.viewport("iphone-6");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-5')
|
cy.viewport("iphone-5");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-4')
|
cy.viewport("iphone-4");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-3')
|
cy.viewport("iphone-3");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
|
|
||||||
// cy.viewport() accepts an orientation for all presets
|
// cy.viewport() accepts an orientation for all presets
|
||||||
// the default orientation is 'portrait'
|
// the default orientation is 'portrait'
|
||||||
cy.viewport('ipad-2', 'portrait')
|
cy.viewport("ipad-2", "portrait");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
cy.viewport('iphone-4', 'landscape')
|
cy.viewport("iphone-4", "landscape");
|
||||||
cy.wait(200)
|
cy.wait(200);
|
||||||
|
|
||||||
// The viewport will be reset back to the default dimensions
|
// The viewport will be reset back to the default dimensions
|
||||||
// in between tests (the default can be set in cypress.json)
|
// in between tests (the default can be set in cypress.json)
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Waiting', () => {
|
context("Waiting", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/waiting')
|
cy.visit("https://example.cypress.io/commands/waiting");
|
||||||
})
|
});
|
||||||
// BE CAREFUL of adding unnecessary wait times.
|
// BE CAREFUL of adding unnecessary wait times.
|
||||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
// https://on.cypress.io/wait
|
||||||
it('cy.wait() - wait for a specific amount of time', () => {
|
it("cy.wait() - wait for a specific amount of time", () => {
|
||||||
cy.get('.wait-input1').type('Wait 1000ms after typing')
|
cy.get(".wait-input1").type("Wait 1000ms after typing");
|
||||||
cy.wait(1000)
|
cy.wait(1000);
|
||||||
cy.get('.wait-input2').type('Wait 1000ms after typing')
|
cy.get(".wait-input2").type("Wait 1000ms after typing");
|
||||||
cy.wait(1000)
|
cy.wait(1000);
|
||||||
cy.get('.wait-input3').type('Wait 1000ms after typing')
|
cy.get(".wait-input3").type("Wait 1000ms after typing");
|
||||||
cy.wait(1000)
|
cy.wait(1000);
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.wait() - wait for a specific route', () => {
|
it("cy.wait() - wait for a specific route", () => {
|
||||||
// Listen to GET to comments/1
|
// Listen to GET to comments/1
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
cy.intercept("GET", "**/comments/*").as("getComment");
|
||||||
|
|
||||||
// we have code that gets a comment when
|
// we have code that gets a comment when
|
||||||
// the button is clicked in scripts.js
|
// the button is clicked in scripts.js
|
||||||
cy.get('.network-btn').click()
|
cy.get(".network-btn").click();
|
||||||
|
|
||||||
// wait for GET comments/1
|
// wait for GET comments/1
|
||||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
context('Window', () => {
|
context("Window", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('https://example.cypress.io/commands/window')
|
cy.visit("https://example.cypress.io/commands/window");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.window() - get the global window object', () => {
|
it("cy.window() - get the global window object", () => {
|
||||||
// https://on.cypress.io/window
|
// https://on.cypress.io/window
|
||||||
cy.window().should('have.property', 'top')
|
cy.window().should("have.property", "top");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.document() - get the document object', () => {
|
it("cy.document() - get the document object", () => {
|
||||||
// https://on.cypress.io/document
|
// https://on.cypress.io/document
|
||||||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
|
cy.document().should("have.property", "charset").and("eq", "UTF-8");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('cy.title() - get the title', () => {
|
it("cy.title() - get the title", () => {
|
||||||
// https://on.cypress.io/title
|
// https://on.cypress.io/title
|
||||||
cy.title().should('include', 'Kitchen Sink')
|
cy.title().should("include", "Kitchen Sink");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"id": 8739,
|
"id": 8739,
|
||||||
"name": "Jane",
|
"name": "Jane",
|
||||||
"email": "jane@example.com"
|
"email": "jane@example.com"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
module.exports = (on, config) => {
|
module.exports = (on, config) => {
|
||||||
// `on` is used to hook into various events Cypress emits
|
// `on` is used to hook into various events Cypress emits
|
||||||
// `config` is the resolved Cypress config
|
// `config` is the resolved Cypress config
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands'
|
import "./commands";
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// require('./commands')
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"baseUrl": "../node_modules",
|
"baseUrl": "../node_modules",
|
||||||
"types": [
|
"types": ["cypress"]
|
||||||
"cypress"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.*"]
|
||||||
"**/*.*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|||||||
@@ -21,22 +21,20 @@ if (!self.define) {
|
|||||||
|
|
||||||
const singleRequire = (uri, parentUri) => {
|
const singleRequire = (uri, parentUri) => {
|
||||||
uri = new URL(uri + ".js", parentUri).href;
|
uri = new URL(uri + ".js", parentUri).href;
|
||||||
return registry[uri] || (
|
return (
|
||||||
|
registry[uri] ||
|
||||||
new Promise(resolve => {
|
new Promise((resolve) => {
|
||||||
if ("document" in self) {
|
if ("document" in self) {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = uri;
|
script.src = uri;
|
||||||
script.onload = resolve;
|
script.onload = resolve;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
} else {
|
} else {
|
||||||
nextDefineUri = uri;
|
nextDefineUri = uri;
|
||||||
importScripts(uri);
|
importScripts(uri);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
})
|
}).then(() => {
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
let promise = registry[uri];
|
let promise = registry[uri];
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
throw new Error(`Module ${uri} didn’t register its module`);
|
throw new Error(`Module ${uri} didn’t register its module`);
|
||||||
@@ -53,21 +51,20 @@ if (!self.define) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let exports = {};
|
let exports = {};
|
||||||
const require = depUri => singleRequire(depUri, uri);
|
const require = (depUri) => singleRequire(depUri, uri);
|
||||||
const specialDeps = {
|
const specialDeps = {
|
||||||
module: { uri },
|
module: { uri },
|
||||||
exports,
|
exports,
|
||||||
require
|
require
|
||||||
};
|
};
|
||||||
registry[uri] = Promise.all(depsNames.map(
|
registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
|
||||||
depName => specialDeps[depName] || require(depName)
|
|
||||||
)).then(deps => {
|
|
||||||
factory(...deps);
|
factory(...deps);
|
||||||
return exports;
|
return exports;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
|
define(["./workbox-b5f7729d"], function (workbox) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
workbox.clientsClaim();
|
workbox.clientsClaim();
|
||||||
@@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
|
|||||||
* requests for URLs in the manifest.
|
* requests for URLs in the manifest.
|
||||||
* See https://goo.gl/S9QRab
|
* See https://goo.gl/S9QRab
|
||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute([{
|
workbox.precacheAndRoute(
|
||||||
"url": "registerSW.js",
|
[
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
{
|
||||||
}, {
|
url: "registerSW.js",
|
||||||
"url": "index.html",
|
revision: "3ca0b8505b4bec776b69afdba2768812"
|
||||||
"revision": "0.sa702m4aq68"
|
},
|
||||||
}], {});
|
{
|
||||||
|
url: "index.html",
|
||||||
|
revision: "0.sa702m4aq68"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{}
|
||||||
|
);
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(
|
||||||
allowlist: [/^\/$/]
|
new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
}));
|
allowlist: [/^\/$/]
|
||||||
|
})
|
||||||
}));
|
);
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,58 @@
|
|||||||
import {ApolloProvider} from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
|
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
|
||||||
import {ConfigProvider} from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import dayjs from "../utils/day";
|
import dayjs from "../utils/day";
|
||||||
import 'dayjs/locale/en';
|
import "dayjs/locale/en";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
import themeProvider from "./themeProvider";
|
import themeProvider from "./themeProvider";
|
||||||
import { Userpilot } from 'userpilot'
|
import { Userpilot } from "userpilot";
|
||||||
|
|
||||||
// Initialize Userpilot
|
// Initialize Userpilot
|
||||||
if(import.meta.env.DEV){
|
if (import.meta.env.DEV) {
|
||||||
Userpilot.initialize('NX-69145f08');
|
Userpilot.initialize("NX-69145f08");
|
||||||
}
|
}
|
||||||
|
|
||||||
dayjs.locale("en");
|
dayjs.locale("en");
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
core: {
|
core: {
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
key: "anon",
|
key: "anon"
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
export const factory = SplitSdk(config);
|
export const factory = SplitSdk(config);
|
||||||
|
|
||||||
|
|
||||||
function AppContainer() {
|
function AppContainer() {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
//componentSize="small"
|
//componentSize="small"
|
||||||
input={{autoComplete: "new-password"}}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
theme={themeProvider}
|
theme={themeProvider}
|
||||||
form={{
|
form={{
|
||||||
validateMessages: {
|
validateMessages: {
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
required: t("general.validation.required", {label: "${label}"}),
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlobalLoadingBar/>
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider factory={factory}>
|
<SplitFactoryProvider factory={factory}>
|
||||||
<App/>
|
<App />
|
||||||
</SplitFactoryProvider>
|
</SplitFactoryProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
export default Sentry.withProfiler(AppContainer);
|
||||||
|
|||||||
@@ -17,245 +17,225 @@ import TechPageContainer from "../pages/tech/tech.page.container";
|
|||||||
import { setOnline } from "../redux/application/application.actions";
|
import { setOnline } from "../redux/application/application.actions";
|
||||||
import { selectOnline } from "../redux/application/application.selectors";
|
import { selectOnline } from "../redux/application/application.selectors";
|
||||||
import { checkUserSession } from "../redux/user/user.actions";
|
import { checkUserSession } from "../redux/user/user.actions";
|
||||||
import {
|
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
||||||
selectBodyshop,
|
|
||||||
selectCurrentEula,
|
|
||||||
selectCurrentUser,
|
|
||||||
} from "../redux/user/user.selectors";
|
|
||||||
import PrivateRoute from "../components/PrivateRoute";
|
import PrivateRoute from "../components/PrivateRoute";
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
import handleBeta from "../utils/betaHandler";
|
import handleBeta from "../utils/betaHandler";
|
||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import { ProductFruits } from 'react-product-fruits';
|
import { ProductFruits } from "react-product-fruits";
|
||||||
|
|
||||||
const ResetPassword = lazy(() =>
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
import("../pages/reset-password/reset-password.component")
|
|
||||||
);
|
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||||
|
|
||||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||||
const MobilePaymentContainer = lazy(() =>
|
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
||||||
import("../pages/mobile-payment/mobile-payment.container")
|
|
||||||
);
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
online: selectOnline,
|
online: selectOnline,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentEula: selectCurrentEula,
|
currentEula: selectCurrentEula
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function App({
|
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
|
||||||
bodyshop,
|
const client = useSplitClient().client;
|
||||||
checkUserSession,
|
const [listenersAdded, setListenersAdded] = useState(false);
|
||||||
currentUser,
|
const { t } = useTranslation();
|
||||||
online,
|
|
||||||
setOnline,
|
|
||||||
currentEula,
|
|
||||||
}) {
|
|
||||||
const client = useSplitClient().client;
|
|
||||||
const [listenersAdded, setListenersAdded] = useState(false);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setOnline(false);
|
setOnline(false);
|
||||||
}
|
|
||||||
|
|
||||||
checkUserSession();
|
|
||||||
}, [checkUserSession, setOnline]);
|
|
||||||
|
|
||||||
//const b = Grid.useBreakpoint();
|
|
||||||
// console.log("Breakpoints:", b);
|
|
||||||
|
|
||||||
// Associate event listeners, memoize to prevent multiple listeners being added
|
|
||||||
useEffect(() => {
|
|
||||||
const offlineListener = (e) => {
|
|
||||||
setOnline(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onlineListener = (e) => {
|
|
||||||
setOnline(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!listenersAdded) {
|
|
||||||
console.log("Added events for offline and online");
|
|
||||||
window.addEventListener("offline", offlineListener);
|
|
||||||
window.addEventListener("online", onlineListener);
|
|
||||||
setListenersAdded(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("offline", offlineListener);
|
|
||||||
window.removeEventListener("online", onlineListener);
|
|
||||||
};
|
|
||||||
}, [setOnline, listenersAdded]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentUser.authorized && bodyshop) {
|
|
||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
|
||||||
|
|
||||||
if (
|
|
||||||
client.getTreatment("LogRocket_Tracking") === "on" ||
|
|
||||||
window.location.hostname ===
|
|
||||||
InstanceRenderMgr({
|
|
||||||
imex: "beta.imex.online",
|
|
||||||
rome: "beta.romeonline.io",
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
console.log("LR Start");
|
|
||||||
LogRocket.init(
|
|
||||||
InstanceRenderMgr({
|
|
||||||
imex: "gvfvfw/bodyshopapp",
|
|
||||||
rome: "rome-online/rome-online",
|
|
||||||
promanager: "", //TODO:AIO Add in log rocket for promanager instances.
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [bodyshop, client, currentUser.authorized]);
|
|
||||||
|
|
||||||
if (currentUser.authorized === null) {
|
|
||||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBeta();
|
checkUserSession();
|
||||||
|
}, [checkUserSession, setOnline]);
|
||||||
|
|
||||||
if (!online)
|
//const b = Grid.useBreakpoint();
|
||||||
return (
|
// console.log("Breakpoints:", b);
|
||||||
<Result
|
|
||||||
status="warning"
|
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||||
title={t("general.labels.nointernet")}
|
useEffect(() => {
|
||||||
subTitle={t("general.labels.nointernet_sub")}
|
const offlineListener = (e) => {
|
||||||
extra={
|
setOnline(false);
|
||||||
<Button
|
};
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
const onlineListener = (e) => {
|
||||||
window.location.reload();
|
setOnline(true);
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
{t("general.actions.refresh")}
|
if (!listenersAdded) {
|
||||||
</Button>
|
console.log("Added events for offline and online");
|
||||||
}
|
window.addEventListener("offline", offlineListener);
|
||||||
/>
|
window.addEventListener("online", onlineListener);
|
||||||
|
setListenersAdded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("offline", offlineListener);
|
||||||
|
window.removeEventListener("online", onlineListener);
|
||||||
|
};
|
||||||
|
}, [setOnline, listenersAdded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser.authorized && bodyshop) {
|
||||||
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
|
|
||||||
|
if (
|
||||||
|
client.getTreatment("LogRocket_Tracking") === "on" ||
|
||||||
|
window.location.hostname ===
|
||||||
|
InstanceRenderMgr({
|
||||||
|
imex: "beta.imex.online",
|
||||||
|
rome: "beta.romeonline.io"
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
console.log("LR Start");
|
||||||
|
LogRocket.init(
|
||||||
|
InstanceRenderMgr({
|
||||||
|
imex: "gvfvfw/bodyshopapp",
|
||||||
|
rome: "rome-online/rome-online",
|
||||||
|
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
|
||||||
return <Eula />;
|
|
||||||
}
|
}
|
||||||
|
}, [bodyshop, client, currentUser.authorized]);
|
||||||
|
|
||||||
// Any route that is not assigned and matched will default to the Landing Page component
|
if (currentUser.authorized === null) {
|
||||||
|
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBeta();
|
||||||
|
|
||||||
|
if (!online)
|
||||||
return (
|
return (
|
||||||
|
<Result
|
||||||
<Suspense
|
status="warning"
|
||||||
fallback={
|
title={t("general.labels.nointernet")}
|
||||||
<LoadingSpinner
|
subTitle={t("general.labels.nointernet_sub")}
|
||||||
message={InstanceRenderMgr({
|
extra={
|
||||||
imex: t("titles.imexonline"),
|
<Button
|
||||||
rome: t("titles.romeonline"),
|
type="primary"
|
||||||
promanager: t("titles.promanager")
|
onClick={() => {
|
||||||
})}
|
window.location.reload();
|
||||||
/>
|
}}
|
||||||
}
|
>
|
||||||
>
|
{t("general.actions.refresh")}
|
||||||
<ProductFruits
|
</Button>
|
||||||
workspaceCode={InstanceRenderMgr({imex:null, rome: null, promanager:"aoJoEifvezYI0Z0P"})}
|
}
|
||||||
debug
|
/>
|
||||||
|
|
||||||
language="en" user={{
|
|
||||||
email: currentUser.email,
|
|
||||||
username: currentUser.email,
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<LandingPage />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/signin"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<SignInPage />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/resetpassword"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<ResetPassword />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/csi/:surveyId"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<CsiPage />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/disclaimer"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<DisclaimerPage />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/mp/:paymentIs"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<MobilePaymentContainer />
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/manage/*"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<PrivateRoute
|
|
||||||
isAuthorized={currentUser.authorized}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="*" element={<ManagePage />} />
|
|
||||||
</Route>
|
|
||||||
<Route
|
|
||||||
path="/tech/*"
|
|
||||||
element={
|
|
||||||
<ErrorBoundary>
|
|
||||||
<PrivateRoute
|
|
||||||
isAuthorized={currentUser.authorized}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="*" element={<TechPageContainer />} />
|
|
||||||
</Route>
|
|
||||||
<Route
|
|
||||||
path="/edit/*"
|
|
||||||
element={
|
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="*" element={<DocumentEditorContainer />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||||
|
return <Eula />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any route that is not assigned and matched will default to the Landing Page component
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<LoadingSpinner
|
||||||
|
message={InstanceRenderMgr({
|
||||||
|
imex: t("titles.imexonline"),
|
||||||
|
rome: t("titles.romeonline"),
|
||||||
|
promanager: t("titles.promanager")
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProductFruits
|
||||||
|
workspaceCode={InstanceRenderMgr({
|
||||||
|
imex: null,
|
||||||
|
rome: null,
|
||||||
|
promanager: "aoJoEifvezYI0Z0P"
|
||||||
|
})}
|
||||||
|
debug
|
||||||
|
language="en"
|
||||||
|
user={{
|
||||||
|
email: currentUser.email,
|
||||||
|
username: currentUser.email
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<LandingPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/signin"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<SignInPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/resetpassword"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ResetPassword />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/csi/:surveyId"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<CsiPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/disclaimer"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<DisclaimerPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/mp/:paymentIs"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<MobilePaymentContainer />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/manage/*"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route path="*" element={<ManagePage />} />
|
||||||
|
</Route>
|
||||||
|
<Route
|
||||||
|
path="/tech/*"
|
||||||
|
element={
|
||||||
|
<ErrorBoundary>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route path="*" element={<TechPageContainer />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
|
||||||
|
<Route path="*" element={<DocumentEditorContainer />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
|
|||||||
@@ -154,7 +154,6 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
||||||
background-color: #f4f4f4;
|
background-color: #f4f4f4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {defaultsDeep} from "lodash";
|
import { defaultsDeep } from "lodash";
|
||||||
import {theme} from "antd";
|
import { theme } from "antd";
|
||||||
import InstanceRenderMgr from '../utils/instanceRenderMgr'
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
|
|
||||||
const {defaultAlgorithm, darkAlgorithm} = theme;
|
const { defaultAlgorithm, darkAlgorithm } = theme;
|
||||||
|
|
||||||
let isDarkMode = false;
|
let isDarkMode = false;
|
||||||
|
|
||||||
@@ -13,28 +13,28 @@ let isDarkMode = false;
|
|||||||
const defaultTheme = {
|
const defaultTheme = {
|
||||||
components: {
|
components: {
|
||||||
Table: {
|
Table: {
|
||||||
rowHoverBg: '#e7f3ff',
|
rowHoverBg: "#e7f3ff",
|
||||||
rowSelectedBg: '#e6f7ff',
|
rowSelectedBg: "#e6f7ff",
|
||||||
headerSortHoverBg: 'transparent',
|
headerSortHoverBg: "transparent"
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
darkItemHoverBg: '#1890ff',
|
darkItemHoverBg: "#1890ff",
|
||||||
itemHoverBg: '#1890ff',
|
itemHoverBg: "#1890ff",
|
||||||
horizontalItemHoverBg: '#1890ff',
|
horizontalItemHoverBg: "#1890ff"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: InstanceRenderMgr({
|
colorPrimary: InstanceRenderMgr({
|
||||||
imex: '#1890ff',
|
imex: "#1890ff",
|
||||||
rome: '#326ade',
|
rome: "#326ade",
|
||||||
promanager:"#1d69a6"
|
promanager: "#1d69a6"
|
||||||
}),
|
}),
|
||||||
colorInfo: InstanceRenderMgr({
|
colorInfo: InstanceRenderMgr({
|
||||||
imex: '#1890ff',
|
imex: "#1890ff",
|
||||||
rome: '#326ade',
|
rome: "#326ade",
|
||||||
promanager:"#1d69a6"
|
promanager: "#1d69a6"
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,16 +42,16 @@ const defaultTheme = {
|
|||||||
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
||||||
*/
|
*/
|
||||||
const devTheme = {
|
const devTheme = {
|
||||||
components: {
|
components: {
|
||||||
Menu: {
|
Menu: {
|
||||||
darkItemHoverBg: '#a51d1d',
|
darkItemHoverBg: "#a51d1d",
|
||||||
itemHoverBg: '#a51d1d',
|
itemHoverBg: "#a51d1d",
|
||||||
horizontalItemHoverBg: '#a51d1d',
|
horizontalItemHoverBg: "#a51d1d"
|
||||||
}
|
|
||||||
},
|
|
||||||
token: {
|
|
||||||
colorPrimary: '#a51d1d'
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
colorPrimary: "#a51d1d"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,11 +60,10 @@ const devTheme = {
|
|||||||
*/
|
*/
|
||||||
const prodTheme = {};
|
const prodTheme = {};
|
||||||
|
|
||||||
const currentTheme = import.meta.env.DEV ? devTheme
|
const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
|
||||||
: prodTheme;
|
|
||||||
|
|
||||||
const finaltheme = {
|
const finaltheme = {
|
||||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||||
...defaultsDeep(currentTheme, defaultTheme)
|
...defaultsDeep(currentTheme, defaultTheme)
|
||||||
}
|
};
|
||||||
export default finaltheme;
|
export default finaltheme;
|
||||||
|
|||||||
@@ -131,4 +131,4 @@
|
|||||||
"author": "iconkitchen",
|
"author": "iconkitchen",
|
||||||
"version": 1
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, {useEffect} from "react";
|
import React, { useEffect } from "react";
|
||||||
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
|
function PrivateRoute({ component: Component, isAuthorized, ...rest }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthorized) {
|
if (!isAuthorized) {
|
||||||
navigate(`/signin?redirect=${location.pathname}`);
|
navigate(`/signin?redirect=${location.pathname}`);
|
||||||
}
|
}
|
||||||
}, [isAuthorized, navigate, location]);
|
}, [isAuthorized, navigate, location]);
|
||||||
|
|
||||||
return <Outlet/>;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PrivateRoute;
|
export default PrivateRoute;
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import {Button} from "antd";
|
import { Button } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setRefundPaymentContext: (context) =>
|
setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
|
||||||
dispatch(setModalContext({context: context, modal: "refund_payment"})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Test({setRefundPaymentContext, refundPaymentModal}) {
|
function Test({ setRefundPaymentContext, refundPaymentModal }) {
|
||||||
console.log("refundPaymentModal", refundPaymentModal);
|
console.log("refundPaymentModal", refundPaymentModal);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setRefundPaymentContext({
|
setRefundPaymentContext({
|
||||||
context: {},
|
context: {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Open Modal
|
Open Modal
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string";
|
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import queryString from "query-string";
|
||||||
import {useTranslation} from "react-i18next";
|
import React, { useState } from "react";
|
||||||
import {connect } from "react-redux";
|
import { useTranslation } from "react-i18next";
|
||||||
import {Link} from "react-router-dom";
|
import { connect } from "react-redux";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {DateFormatter} from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||||
@@ -17,216 +18,179 @@ import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-
|
|||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(AccountingPayablesTableComponent);
|
|
||||||
|
|
||||||
export function AccountingPayablesTableComponent({
|
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
loading,
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
bills,
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
refetch,
|
const [state, setState] = useState({
|
||||||
}) {
|
sortedInfo: {},
|
||||||
const {t} = useTranslation();
|
search: ""
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
});
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
|
||||||
const [state, setState] = useState({
|
|
||||||
sortedInfo: {},
|
|
||||||
search: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("bills.fields.vendorname"),
|
title: t("bills.fields.vendorname"),
|
||||||
dataIndex: "vendorname",
|
dataIndex: "vendorname",
|
||||||
key: "vendorname",
|
key: "vendorname",
|
||||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
render: (text, record) => (
|
||||||
render: (text, record) => (
|
<Link
|
||||||
<Link
|
to={{
|
||||||
to={{
|
pathname: `/manage/shop/vendors`,
|
||||||
pathname: `/manage/shop/vendors`,
|
search: queryString.stringify({ selectedvendor: record.vendor.id })
|
||||||
search: queryString.stringify({selectedvendor: record.vendor.id}),
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
{record.vendor.name}
|
|
||||||
</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,
|
|
||||||
render: (text, record) => (
|
|
||||||
<Link
|
|
||||||
to={{
|
|
||||||
pathname: `/manage/bills`,
|
|
||||||
search: queryString.stringify({
|
|
||||||
billid: record.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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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}/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("exportlogs.labels.attempts"),
|
|
||||||
dataIndex: "attempts",
|
|
||||||
key: "attempts",
|
|
||||||
|
|
||||||
render: (text, record) => (
|
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("general.labels.actions"),
|
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
|
|
||||||
|
|
||||||
render: (text, record) => (
|
|
||||||
<PayableExportButton
|
|
||||||
billId={record.id}
|
|
||||||
disabled={transInProgress || !!record.exported}
|
|
||||||
loadingCallback={setTransInProgress}
|
|
||||||
setSelectedBills={setSelectedBills}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
|
||||||
setState({...state, search: e.target.value});
|
|
||||||
logImEXEvent("accounting_payables_table_search");
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataSource = state.search
|
|
||||||
? bills.filter(
|
|
||||||
(v) =>
|
|
||||||
(v.vendor.name || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.invoice_number || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase())
|
|
||||||
)
|
|
||||||
: bills;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
extra={
|
|
||||||
<Space wrap>
|
|
||||||
<BillMarkSelectedExported
|
|
||||||
billids={selectedBills}
|
|
||||||
disabled={transInProgress || selectedBills.length === 0}
|
|
||||||
loadingCallback={setTransInProgress}
|
|
||||||
completedCallback={setSelectedBills}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
<PayableExportAll
|
|
||||||
billids={selectedBills}
|
|
||||||
disabled={transInProgress || selectedBills.length === 0}
|
|
||||||
loadingCallback={setTransInProgress}
|
|
||||||
completedCallback={setSelectedBills}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
|
||||||
<QboAuthorizeComponent/>
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
value={state.search}
|
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Table
|
{record.vendor.name}
|
||||||
loading={loading}
|
</Link>
|
||||||
dataSource={dataSource}
|
)
|
||||||
pagination={{position: "top", pageSize: pageLimit}}
|
},
|
||||||
columns={columns}
|
{
|
||||||
rowKey="id"
|
title: t("bills.fields.invoice_number"),
|
||||||
onChange={handleTableChange}
|
dataIndex: "invoice_number",
|
||||||
rowSelection={{
|
key: "invoice_number",
|
||||||
onSelectAll: (selected, selectedRows) =>
|
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||||
setSelectedBills(selectedRows.map((i) => i.id)),
|
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
render: (text, record) => (
|
||||||
setSelectedBills(selectedRows.map((i) => i.id));
|
<Link
|
||||||
},
|
to={{
|
||||||
getCheckboxProps: (record) => ({
|
pathname: `/manage/bills`,
|
||||||
disabled: record.exported,
|
search: queryString.stringify({
|
||||||
}),
|
billid: record.id,
|
||||||
selectedRowKeys: selectedBills,
|
vendorid: record.vendor.id
|
||||||
type: "checkbox",
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Card>
|
{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>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("exportlogs.labels.attempts"),
|
||||||
|
dataIndex: "attempts",
|
||||||
|
key: "attempts",
|
||||||
|
|
||||||
|
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<PayableExportButton
|
||||||
|
billId={record.id}
|
||||||
|
disabled={transInProgress || !!record.exported}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
setSelectedBills={setSelectedBills}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
setState({ ...state, search: e.target.value });
|
||||||
|
logImEXEvent("accounting_payables_table_search");
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSource = state.search
|
||||||
|
? bills.filter(
|
||||||
|
(v) =>
|
||||||
|
(v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
(v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
|
)
|
||||||
|
: bills;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<BillMarkSelectedExported
|
||||||
|
billids={selectedBills}
|
||||||
|
disabled={transInProgress || selectedBills.length === 0}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
completedCallback={setSelectedBills}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<PayableExportAll
|
||||||
|
billids={selectedBills}
|
||||||
|
disabled={transInProgress || selectedBills.length === 0}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
completedCallback={setSelectedBills}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
|
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
dataSource={dataSource}
|
||||||
|
pagination={{ position: "top", pageSize: pageLimit }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
selectedRowKeys: selectedBills,
|
||||||
|
type: "checkbox"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,242 +1,198 @@
|
|||||||
import {Card, Input, Space, Table} from "antd";
|
import { Card, Input, Space, Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {Link} from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import {pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, {
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../owner-name-display/owner-name-display.component";
|
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(AccountingPayablesTableComponent);
|
|
||||||
|
|
||||||
export function AccountingPayablesTableComponent({
|
export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
loading,
|
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||||
payments,
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
refetch,
|
const [state, setState] = useState({
|
||||||
}) {
|
sortedInfo: {},
|
||||||
const {t} = useTranslation();
|
search: ""
|
||||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
});
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
|
||||||
const [state, setState] = useState({
|
|
||||||
sortedInfo: {},
|
|
||||||
search: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.ro_number"),
|
title: t("jobs.fields.ro_number"),
|
||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
key: "ro_number",
|
key: "ro_number",
|
||||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
render: (text, record) => <Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
||||||
render: (text, record) => (
|
},
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
{
|
||||||
),
|
title: t("payments.fields.date"),
|
||||||
},
|
dataIndex: "date",
|
||||||
{
|
key: "date",
|
||||||
title: t("payments.fields.date"),
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
dataIndex: "date",
|
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
key: "date",
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
|
||||||
sorter: (a, b) => dateSort(a.date, b.date),
|
},
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
key: "owner",
|
key: "owner",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) =>
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
|
||||||
alphaSort(
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
OwnerNameDisplayFunction(a.job),
|
render: (text, record) => {
|
||||||
OwnerNameDisplayFunction(b.job)
|
return record.job.owner ? (
|
||||||
),
|
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||||
sortOrder:
|
<OwnerNameDisplay ownerObject={record.job} />
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
</Link>
|
||||||
render: (text, record) => {
|
) : (
|
||||||
return record.job.owner ? (
|
<span>
|
||||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
<OwnerNameDisplay ownerObject={record.job} />
|
||||||
<OwnerNameDisplay ownerObject={record.job}/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<OwnerNameDisplay ownerObject={record.job}/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.amount"),
|
title: t("payments.fields.amount"),
|
||||||
dataIndex: "amount",
|
dataIndex: "amount",
|
||||||
key: "amount",
|
key: "amount",
|
||||||
sorter: (a, b) => a.amount - b.amount,
|
sorter: (a, b) => a.amount - b.amount,
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => (
|
render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
},
|
||||||
),
|
{
|
||||||
},
|
title: t("payments.fields.memo"),
|
||||||
{
|
dataIndex: "memo",
|
||||||
title: t("payments.fields.memo"),
|
key: "memo"
|
||||||
dataIndex: "memo",
|
},
|
||||||
key: "memo",
|
{
|
||||||
},
|
title: t("payments.fields.transactionid"),
|
||||||
{
|
dataIndex: "transactionid",
|
||||||
title: t("payments.fields.transactionid"),
|
key: "transactionid"
|
||||||
dataIndex: "transactionid",
|
},
|
||||||
key: "transactionid",
|
{
|
||||||
},
|
title: t("payments.fields.created_at"),
|
||||||
{
|
dataIndex: "created_at",
|
||||||
title: t("payments.fields.created_at"),
|
key: "created_at",
|
||||||
dataIndex: "created_at",
|
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||||
key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||||
sortOrder:
|
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
},
|
||||||
render: (text, record) => (
|
//{
|
||||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
// title: t("payments.fields.exportedat"),
|
||||||
),
|
// dataIndex: "exportedat",
|
||||||
},
|
// key: "exportedat",
|
||||||
//{
|
// render: (text, record) => (
|
||||||
// title: t("payments.fields.exportedat"),
|
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||||
// dataIndex: "exportedat",
|
// ),
|
||||||
// key: "exportedat",
|
//},
|
||||||
// render: (text, record) => (
|
{
|
||||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
title: t("exportlogs.labels.attempts"),
|
||||||
// ),
|
dataIndex: "attempts",
|
||||||
//},
|
key: "attempts",
|
||||||
{
|
|
||||||
title: t("exportlogs.labels.attempts"),
|
|
||||||
dataIndex: "attempts",
|
|
||||||
key: "attempts",
|
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
},
|
||||||
),
|
{
|
||||||
},
|
title: t("general.labels.actions"),
|
||||||
{
|
dataIndex: "actions",
|
||||||
title: t("general.labels.actions"),
|
key: "actions",
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<PaymentExportButton
|
||||||
|
paymentId={record.id}
|
||||||
|
disabled={transInProgress || !!record.exportedat}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
setSelectedPayments={setSelectedPayments}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
render: (text, record) => (
|
const handleSearch = (e) => {
|
||||||
<PaymentExportButton
|
setState({ ...state, search: e.target.value });
|
||||||
paymentId={record.id}
|
logImEXEvent("account_payments_table_search");
|
||||||
disabled={transInProgress || !!record.exportedat}
|
};
|
||||||
loadingCallback={setTransInProgress}
|
|
||||||
setSelectedPayments={setSelectedPayments}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const dataSource = state.search
|
||||||
setState({...state, search: e.target.value});
|
? payments.filter(
|
||||||
logImEXEvent("account_payments_table_search");
|
(v) =>
|
||||||
};
|
(v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
|
)
|
||||||
|
: payments;
|
||||||
|
|
||||||
const dataSource = state.search
|
return (
|
||||||
? payments.filter(
|
<Card
|
||||||
(v) =>
|
extra={
|
||||||
(v.paymentnum || "")
|
<Space wrap>
|
||||||
.toLowerCase()
|
<PaymentMarkSelectedExported
|
||||||
.includes(state.search.toLowerCase()) ||
|
paymentIds={selectedPayments}
|
||||||
((v.job && v.job.ro_number) || "")
|
disabled={transInProgress || selectedPayments.length === 0}
|
||||||
.toLowerCase()
|
loadingCallback={setTransInProgress}
|
||||||
.includes(state.search.toLowerCase()) ||
|
completedCallback={setSelectedPayments}
|
||||||
((v.job && v.job.ownr_fn) || "")
|
refetch={refetch}
|
||||||
.toLowerCase()
|
/>
|
||||||
.includes(state.search.toLowerCase()) ||
|
<PaymentsExportAllButton
|
||||||
((v.job && v.job.ownr_ln) || "")
|
paymentIds={selectedPayments}
|
||||||
.toLowerCase()
|
disabled={transInProgress || selectedPayments.length === 0}
|
||||||
.includes(state.search.toLowerCase()) ||
|
loadingCallback={setTransInProgress}
|
||||||
((v.job && v.job.ownr_co_nm) || "")
|
completedCallback={setSelectedPayments}
|
||||||
.toLowerCase()
|
refetch={refetch}
|
||||||
.includes(state.search.toLowerCase())
|
/>
|
||||||
)
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
: payments;
|
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||||
|
</Space>
|
||||||
return (
|
}
|
||||||
<Card
|
>
|
||||||
extra={
|
<Table
|
||||||
<Space wrap>
|
loading={loading}
|
||||||
<PaymentMarkSelectedExported
|
dataSource={dataSource}
|
||||||
paymentIds={selectedPayments}
|
pagination={{ position: "top", pageSize: pageLimit }}
|
||||||
disabled={transInProgress || selectedPayments.length === 0}
|
columns={columns}
|
||||||
loadingCallback={setTransInProgress}
|
rowKey="id"
|
||||||
completedCallback={setSelectedPayments}
|
onChange={handleTableChange}
|
||||||
refetch={refetch}
|
rowSelection={{
|
||||||
/>
|
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||||
<PaymentsExportAllButton
|
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||||
paymentIds={selectedPayments}
|
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||||
disabled={transInProgress || selectedPayments.length === 0}
|
},
|
||||||
loadingCallback={setTransInProgress}
|
getCheckboxProps: (record) => ({
|
||||||
completedCallback={setSelectedPayments}
|
disabled: record.exported
|
||||||
refetch={refetch}
|
}),
|
||||||
/>
|
selectedRowKeys: selectedPayments,
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
type: "checkbox"
|
||||||
<QboAuthorizeComponent/>
|
}}
|
||||||
)}
|
/>
|
||||||
<Input
|
</Card>
|
||||||
value={state.search}
|
);
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
loading={loading}
|
|
||||||
dataSource={dataSource}
|
|
||||||
pagination={{position: "top", pageSize: pageLimit}}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
onChange={handleTableChange}
|
|
||||||
rowSelection={{
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
selectedRowKeys: selectedPayments,
|
|
||||||
type: "checkbox",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,258 +1,212 @@
|
|||||||
import {Button, Card, Input, Space, Table} from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {Link} from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {DateFormatter} from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, {
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../owner-name-display/owner-name-display.component";
|
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(AccountingReceivablesTableComponent);
|
|
||||||
|
|
||||||
export function AccountingReceivablesTableComponent({
|
export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
loading,
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
jobs,
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
refetch,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: "",
|
search: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.ro_number"),
|
title: t("jobs.fields.ro_number"),
|
||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
key: "ro_number",
|
key: "ro_number",
|
||||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
render: (text, record) => <Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
||||||
render: (text, record) => (
|
},
|
||||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.status"),
|
title: t("jobs.fields.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
},
|
||||||
},
|
{
|
||||||
{
|
title: t("jobs.fields.date_invoiced"),
|
||||||
title: t("jobs.fields.date_invoiced"),
|
dataIndex: "date_invoiced",
|
||||||
dataIndex: "date_invoiced",
|
key: "date_invoiced",
|
||||||
key: "date_invoiced",
|
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
||||||
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
|
||||||
sortOrder:
|
render: (text, record) => <DateFormatter>{record.date_invoiced}</DateFormatter>
|
||||||
state.sortedInfo.columnKey === "date_invoiced" &&
|
},
|
||||||
state.sortedInfo.order,
|
{
|
||||||
render: (text, record) => (
|
title: t("jobs.fields.owner"),
|
||||||
<DateFormatter>{record.date_invoiced}</DateFormatter>
|
dataIndex: "owner",
|
||||||
),
|
key: "owner",
|
||||||
},
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
{
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
title: t("jobs.fields.owner"),
|
render: (text, record) => {
|
||||||
dataIndex: "owner",
|
return record.owner ? (
|
||||||
key: "owner",
|
<Link to={"/manage/owners/" + record.owner.id}>
|
||||||
sorter: (a, b) =>
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
</Link>
|
||||||
sortOrder:
|
) : (
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
<span>
|
||||||
render: (text, record) => {
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
return record.owner ? (
|
|
||||||
<Link to={"/manage/owners/" + record.owner.id}>
|
|
||||||
<OwnerNameDisplay ownerObject={record}/>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<OwnerNameDisplay ownerObject={record}/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.vehicle"),
|
title: t("jobs.fields.vehicle"),
|
||||||
dataIndex: "vehicle",
|
dataIndex: "vehicle",
|
||||||
key: "vehicle",
|
key: "vehicle",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) =>
|
sorter: (a, b) =>
|
||||||
alphaSort(
|
alphaSort(
|
||||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
|
||||||
a.v_model_desc || ""
|
|
||||||
}`,
|
|
||||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
),
|
),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.vehicleid ? (
|
return record.vehicleid ? (
|
||||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||||
record.v_model_desc || ""
|
</Link>
|
||||||
}`}
|
) : (
|
||||||
</Link>
|
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${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"),
|
||||||
},
|
dataIndex: "clm_no",
|
||||||
},
|
key: "clm_no",
|
||||||
{
|
ellipsis: true,
|
||||||
title: t("jobs.fields.clm_no"),
|
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
dataIndex: "clm_no",
|
sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
|
||||||
key: "clm_no",
|
},
|
||||||
ellipsis: true,
|
{
|
||||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
title: t("jobs.fields.clm_total"),
|
||||||
sortOrder:
|
dataIndex: "clm_total",
|
||||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
key: "clm_total",
|
||||||
},
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
{
|
sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||||
title: t("jobs.fields.clm_total"),
|
render: (text, record) => {
|
||||||
dataIndex: "clm_total",
|
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
||||||
key: "clm_total",
|
}
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
},
|
||||||
sortOrder:
|
{
|
||||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
title: t("exportlogs.labels.attempts"),
|
||||||
render: (text, record) => {
|
dataIndex: "attempts",
|
||||||
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
key: "attempts",
|
||||||
},
|
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("exportlogs.labels.attempts"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "actions",
|
||||||
key: "attempts",
|
key: "actions",
|
||||||
render: (text, record) => (
|
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("general.labels.actions"),
|
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<JobExportButton
|
<JobExportButton
|
||||||
jobId={record.id}
|
jobId={record.id}
|
||||||
disabled={!!record.date_exported}
|
disabled={!!record.date_exported}
|
||||||
setSelectedJobs={setSelectedJobs}
|
setSelectedJobs={setSelectedJobs}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
)
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const handleSearch = (e) => {
|
||||||
setState({...state, search: e.target.value});
|
setState({ ...state, search: e.target.value });
|
||||||
logImEXEvent("accounting_receivables_search");
|
logImEXEvent("accounting_receivables_search");
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = state.search
|
const dataSource = state.search
|
||||||
? jobs.filter(
|
? jobs.filter(
|
||||||
(v) =>
|
(v) =>
|
||||||
(v.ro_number || "")
|
(v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
.toString()
|
(v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
.includes(state.search.toLowerCase()) ||
|
(v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
(v.ownr_fn || "")
|
(v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(v.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
.includes(state.search.toLowerCase()) ||
|
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
(v.ownr_ln || "")
|
)
|
||||||
.toLowerCase()
|
: jobs;
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.ownr_co_nm || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.v_model_desc || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.v_make_desc || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
|
||||||
)
|
|
||||||
: jobs;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||||
<JobsExportAllButton
|
<JobsExportAllButton
|
||||||
jobIds={selectedJobs}
|
jobIds={selectedJobs}
|
||||||
disabled={transInProgress || selectedJobs.length === 0}
|
disabled={transInProgress || selectedJobs.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedJobs}
|
completedCallback={setSelectedJobs}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
|
||||||
<QboAuthorizeComponent/>
|
|
||||||
)}
|
|
||||||
<Input.Search
|
|
||||||
value={state.search}
|
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
loading={loading}
|
|
||||||
dataSource={dataSource}
|
|
||||||
pagination={{position: "top"}}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
onChange={handleTableChange}
|
|
||||||
rowSelection={{
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
selectedRowKeys: selectedJobs,
|
|
||||||
type: "checkbox",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
)}
|
||||||
);
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
|
<Input.Search
|
||||||
|
value={state.search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
dataSource={dataSource}
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
selectedRowKeys: selectedJobs,
|
||||||
|
type: "checkbox"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Alert} from "antd";
|
import { Alert } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function AlertComponent(props) {
|
export default function AlertComponent(props) {
|
||||||
return <Alert {...props} />;
|
return <Alert {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import {shallow} from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Alert from "./alert.component";
|
import Alert from "./alert.component";
|
||||||
|
|
||||||
describe("Alert component", () => {
|
describe("Alert component", () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Test error message.",
|
message: "Test error message."
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapper = shallow(<Alert {...mockProps} />);
|
wrapper = shallow(<Alert {...mockProps} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render Alert component", () => {
|
it("should render Alert component", () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,72 +1,67 @@
|
|||||||
import {Button, InputNumber, Popover, Select} from "antd";
|
import { Button, InputNumber, Popover, Select } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AllocationsAssignmentComponent({
|
export function AllocationsAssignmentComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
handleAssignment,
|
handleAssignment,
|
||||||
assignment,
|
assignment,
|
||||||
setAssignment,
|
setAssignment,
|
||||||
visibilityState,
|
visibilityState,
|
||||||
maxHours
|
maxHours
|
||||||
}) {
|
}) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onChange = e => {
|
const onChange = (e) => {
|
||||||
setAssignment({...assignment, employeeid: e});
|
setAssignment({ ...assignment, employeeid: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [visibility, setVisibility] = visibilityState;
|
const [visibility, setVisibility] = visibilityState;
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Select id="employeeSelector"
|
<Select
|
||||||
showSearch
|
id="employeeSelector"
|
||||||
style={{width: 200}}
|
showSearch
|
||||||
placeholder='Select a person'
|
style={{ width: 200 }}
|
||||||
optionFilterProp='children'
|
placeholder="Select a person"
|
||||||
onChange={onChange}
|
optionFilterProp="children"
|
||||||
filterOption={(input, option) =>
|
onChange={onChange}
|
||||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||||
}>
|
>
|
||||||
{bodyshop.employees.map(emp => (
|
{bodyshop.employees.map((emp) => (
|
||||||
<Select.Option value={emp.id} key={emp.id}>
|
<Select.Option value={emp.id} key={emp.id}>
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
defaultValue={assignment.hours}
|
defaultValue={assignment.hours}
|
||||||
placeholder={t("joblines.fields.mod_lb_hrs")}
|
placeholder={t("joblines.fields.mod_lb_hrs")}
|
||||||
max={parseFloat(maxHours)}
|
max={parseFloat(maxHours)}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={e => setAssignment({...assignment, hours: e})}
|
onChange={(e) => setAssignment({ ...assignment, hours: e })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
|
||||||
type='primary'
|
Assign
|
||||||
disabled={!assignment.employeeid}
|
</Button>
|
||||||
onClick={handleAssignment}>
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
Assign
|
</div>
|
||||||
</Button>
|
);
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} open={visibility}>
|
<Popover content={popContent} open={visibility}>
|
||||||
<Button onClick={() => setVisibility(true)}>
|
<Button onClick={() => setVisibility(true)}>{t("allocations.actions.assign")}</Button>
|
||||||
{t("allocations.actions.assign")}
|
</Popover>
|
||||||
</Button>
|
);
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);
|
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import {mount} from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {MockBodyshop} from "../../utils/TestingHelpers";
|
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||||
import {AllocationsAssignmentComponent} from "./allocations-assignment.component";
|
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||||
|
|
||||||
describe("AllocationsAssignmentComponent component", () => {
|
describe("AllocationsAssignmentComponent component", () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
bodyshop: MockBodyshop,
|
bodyshop: MockBodyshop,
|
||||||
handleAssignment: jest.fn(),
|
handleAssignment: jest.fn(),
|
||||||
assignment: {},
|
assignment: {},
|
||||||
setAssignment: jest.fn(),
|
setAssignment: jest.fn(),
|
||||||
visibilityState: [false, jest.fn()],
|
visibilityState: [false, jest.fn()],
|
||||||
maxHours: 4,
|
maxHours: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render AllocationsAssignmentComponent component", () => {
|
it("should render AllocationsAssignmentComponent component", () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render a list of employees", () => {
|
it("should render a list of employees", () => {
|
||||||
const empList = wrapper.find("#employeeSelector");
|
const empList = wrapper.find("#employeeSelector");
|
||||||
expect(empList.children()).to.have.lengthOf(2);
|
expect(empList.children()).to.have.lengthOf(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create an allocation on save", () => {
|
it("should create an allocation on save", () => {
|
||||||
wrapper.find("Button").simulate("click");
|
wrapper.find("Button").simulate("click");
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,47 +1,43 @@
|
|||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
|
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
|
|
||||||
export default function AllocationsAssignmentContainer({
|
export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
|
||||||
jobLineId,
|
const visibilityState = useState(false);
|
||||||
hours,
|
const { t } = useTranslation();
|
||||||
refetch,
|
const [assignment, setAssignment] = useState({
|
||||||
}) {
|
joblineid: jobLineId,
|
||||||
const visibilityState = useState(false);
|
hours: parseFloat(hours),
|
||||||
const {t} = useTranslation();
|
employeeid: null
|
||||||
const [assignment, setAssignment] = useState({
|
});
|
||||||
joblineid: jobLineId,
|
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||||
hours: parseFloat(hours),
|
|
||||||
employeeid: null,
|
|
||||||
});
|
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
|
||||||
|
|
||||||
const handleAssignment = () => {
|
const handleAssignment = () => {
|
||||||
insertAllocation({variables: {alloc: {...assignment}}})
|
insertAllocation({ variables: { alloc: { ...assignment } } })
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("allocations.successes.save"),
|
message: t("allocations.successes.save")
|
||||||
});
|
});
|
||||||
visibilityState[1](false);
|
visibilityState[1](false);
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("employees.errors.saving", {message: error.message}),
|
message: t("employees.errors.saving", { message: error.message })
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AllocationsAssignmentComponent
|
<AllocationsAssignmentComponent
|
||||||
handleAssignment={handleAssignment}
|
handleAssignment={handleAssignment}
|
||||||
maxHours={hours}
|
maxHours={hours}
|
||||||
assignment={assignment}
|
assignment={assignment}
|
||||||
setAssignment={setAssignment}
|
setAssignment={setAssignment}
|
||||||
visibilityState={visibilityState}
|
visibilityState={visibilityState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,62 @@
|
|||||||
import {Button, Popover, Select} from "antd";
|
import { Button, Popover, Select } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
null
|
null
|
||||||
)(function AllocationsBulkAssignmentComponent({
|
)(function AllocationsBulkAssignmentComponent({
|
||||||
disabled,
|
disabled,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
handleAssignment,
|
handleAssignment,
|
||||||
assignment,
|
assignment,
|
||||||
setAssignment,
|
setAssignment,
|
||||||
visibilityState,
|
visibilityState
|
||||||
}) {
|
}) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
setAssignment({...assignment, employeeid: e});
|
setAssignment({ ...assignment, employeeid: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [visibility, setVisibility] = visibilityState;
|
const [visibility, setVisibility] = visibilityState;
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
style={{width: 200}}
|
style={{ width: 200 }}
|
||||||
placeholder="Select a person"
|
placeholder="Select a person"
|
||||||
optionFilterProp="children"
|
optionFilterProp="children"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
>
|
||||||
}
|
{bodyshop.employees.map((emp) => (
|
||||||
>
|
<Select.Option value={emp.id} key={emp.id}>
|
||||||
{bodyshop.employees.map((emp) => (
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
<Select.Option value={emp.id} key={emp.id}>
|
</Select.Option>
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
))}
|
||||||
</Select.Option>
|
</Select>
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
|
||||||
type="primary"
|
Assign
|
||||||
disabled={!assignment.employeeid}
|
</Button>
|
||||||
onClick={handleAssignment}
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
>
|
</div>
|
||||||
Assign
|
);
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} open={visibility}>
|
<Popover content={popContent} open={visibility}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
{t("allocations.actions.assign")}
|
{t("allocations.actions.assign")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
|
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
|
|
||||||
export default function AllocationsBulkAssignmentContainer({
|
export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
|
||||||
jobLines,
|
const visibilityState = useState(false);
|
||||||
refetch,
|
const { t } = useTranslation();
|
||||||
}) {
|
const [assignment, setAssignment] = useState({
|
||||||
const visibilityState = useState(false);
|
employeeid: null
|
||||||
const {t} = useTranslation();
|
});
|
||||||
const [assignment, setAssignment] = useState({
|
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||||
employeeid: null,
|
|
||||||
|
const handleAssignment = () => {
|
||||||
|
const allocs = jobLines.reduce((acc, value) => {
|
||||||
|
acc.push({
|
||||||
|
joblineid: value.id,
|
||||||
|
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||||
|
employeeid: assignment.employeeid
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("employees.successes.save")
|
||||||
|
});
|
||||||
|
visibilityState[1](false);
|
||||||
|
if (refetch) refetch();
|
||||||
});
|
});
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
};
|
||||||
|
|
||||||
const handleAssignment = () => {
|
return (
|
||||||
const allocs = jobLines.reduce((acc, value) => {
|
<AllocationsBulkAssignment
|
||||||
acc.push({
|
disabled={jobLines.length > 0 ? false : true}
|
||||||
joblineid: value.id,
|
handleAssignment={handleAssignment}
|
||||||
hours: parseFloat(value.mod_lb_hrs) || 0,
|
assignment={assignment}
|
||||||
employeeid: assignment.employeeid,
|
setAssignment={setAssignment}
|
||||||
});
|
visibilityState={visibilityState}
|
||||||
return acc;
|
/>
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
insertAllocation({variables: {alloc: allocs}}).then((r) => {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("employees.successes.save"),
|
|
||||||
});
|
|
||||||
visibilityState[1](false);
|
|
||||||
if (refetch) refetch();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AllocationsBulkAssignment
|
|
||||||
disabled={jobLines.length > 0 ? false : true}
|
|
||||||
handleAssignment={handleAssignment}
|
|
||||||
assignment={assignment}
|
|
||||||
setAssignment={setAssignment}
|
|
||||||
visibilityState={visibilityState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {MdRemoveCircleOutline} from "react-icons/md";
|
import { MdRemoveCircleOutline } from "react-icons/md";
|
||||||
|
|
||||||
export default function AllocationsLabelComponent({allocation, handleClick}) {
|
export default function AllocationsLabelComponent({ allocation, handleClick }) {
|
||||||
return (
|
return (
|
||||||
<div style={{display: "flex", alignItems: "center"}}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<span>
|
<span>
|
||||||
{`${allocation.employee.first_name || ""} ${
|
{`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
|
||||||
allocation.employee.last_name || ""
|
|
||||||
} (${allocation.hours || ""})`}
|
|
||||||
</span>
|
</span>
|
||||||
<Icon
|
<Icon style={{ color: "red", padding: "0px 4px" }} component={MdRemoveCircleOutline} onClick={handleClick} />
|
||||||
style={{color: "red", padding: "0px 4px"}}
|
</div>
|
||||||
component={MdRemoveCircleOutline}
|
);
|
||||||
onClick={handleClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {DELETE_ALLOCATION} from "../../graphql/allocations.queries";
|
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function AllocationsLabelContainer({allocation, refetch}) {
|
export default function AllocationsLabelContainer({ allocation, refetch }) {
|
||||||
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
|
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteAllocation({variables: {id: allocation.id}})
|
deleteAllocation({ variables: { id: allocation.id } })
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("allocations.successes.deleted"),
|
message: t("allocations.successes.deleted")
|
||||||
});
|
});
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification["error"]({message: t("allocations.errors.deleting")});
|
notification["error"]({ message: t("allocations.errors.deleting") });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <AllocationsLabelComponent allocation={allocation} handleClick={handleClick} />;
|
||||||
<AllocationsLabelComponent
|
|
||||||
allocation={allocation}
|
|
||||||
handleClick={handleClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,75 @@
|
|||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {Table} from "antd";
|
import { Table } from "antd";
|
||||||
import {alphaSort} from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||||
import {pageLimit} from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
|
|
||||||
export default function AuditTrailListComponent({loading, data}) {
|
export default function AuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: {},
|
filteredInfo: {}
|
||||||
});
|
});
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("audit.fields.created"),
|
title: t("audit.fields.created"),
|
||||||
dataIndex: " created",
|
dataIndex: " created",
|
||||||
key: " created",
|
key: " created",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
render: (text, record) => (
|
render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
|
||||||
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
sorter: (a, b) => a.created - b.created,
|
||||||
),
|
sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||||
sorter: (a, b) => a.created - b.created,
|
},
|
||||||
sortOrder:
|
{
|
||||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
title: t("audit.fields.operation"),
|
||||||
},
|
dataIndex: "operation",
|
||||||
{
|
key: "operation",
|
||||||
title: t("audit.fields.operation"),
|
width: "10%",
|
||||||
dataIndex: "operation",
|
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||||
key: "operation",
|
sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
|
||||||
width: "10%",
|
},
|
||||||
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
{
|
||||||
sortOrder:
|
title: t("audit.fields.values"),
|
||||||
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
|
dataIndex: " old_val",
|
||||||
},
|
key: " old_val",
|
||||||
{
|
width: "10%",
|
||||||
title: t("audit.fields.values"),
|
render: (text, record) => <AuditTrailValuesComponent oldV={record.old_val} newV={record.new_val} />
|
||||||
dataIndex: " old_val",
|
},
|
||||||
key: " old_val",
|
{
|
||||||
width: "10%",
|
title: t("audit.fields.useremail"),
|
||||||
render: (text, record) => (
|
dataIndex: "useremail",
|
||||||
<AuditTrailValuesComponent
|
key: "useremail",
|
||||||
oldV={record.old_val}
|
width: "10%",
|
||||||
newV={record.new_val}
|
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||||
/>
|
sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||||
),
|
}
|
||||||
},
|
];
|
||||||
{
|
|
||||||
title: t("audit.fields.useremail"),
|
|
||||||
dataIndex: "useremail",
|
|
||||||
key: "useremail",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const formItemLayout = {
|
const formItemLayout = {
|
||||||
labelCol: {
|
labelCol: {
|
||||||
xs: {span: 12},
|
xs: { span: 12 },
|
||||||
sm: {span: 5},
|
sm: { span: 5 }
|
||||||
},
|
},
|
||||||
wrapperCol: {
|
wrapperCol: {
|
||||||
xs: {span: 24},
|
xs: { span: 24 },
|
||||||
sm: {span: 12},
|
sm: { span: 12 }
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import AuditTrailListComponent from "./audit-trail-list.component";
|
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||||
import {useQuery} from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries";
|
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
|
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
|
||||||
import {Card, Row} from "antd";
|
import { Card, Row } from "antd";
|
||||||
|
|
||||||
export default function AuditTrailListContainer({recordId}) {
|
export default function AuditTrailListContainer({ recordId }) {
|
||||||
const {loading, error, data} = useQuery(QUERY_AUDIT_TRAIL, {
|
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
variables: {id: recordId},
|
variables: { id: recordId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
logImEXEvent("audittrail_view", {recordId});
|
logImEXEvent("audittrail_view", { recordId });
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<AlertComponent type="error" message={error.message}/>
|
<AlertComponent type="error" message={error.message} />
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Card>
|
<Card>
|
||||||
<AuditTrailListComponent
|
<AuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
|
||||||
loading={loading}
|
</Card>
|
||||||
data={data ? data.audit_trail : []}
|
<Card>
|
||||||
/>
|
<EmailAuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
</Row>
|
||||||
<EmailAuditTrailListComponent
|
)}
|
||||||
loading={loading}
|
</div>
|
||||||
data={data ? data.audit_trail : []}
|
);
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,60 @@
|
|||||||
import {Table} from "antd";
|
import { Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import {alphaSort} from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import {pageLimit} from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
|
|
||||||
export default function EmailAuditTrailListComponent({loading, data}) {
|
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: {},
|
filteredInfo: {}
|
||||||
});
|
});
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("audit.fields.created"),
|
title: t("audit.fields.created"),
|
||||||
dataIndex: " created",
|
dataIndex: " created",
|
||||||
key: " created",
|
key: " created",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
render: (text, record) => (
|
render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
|
||||||
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
sorter: (a, b) => a.created - b.created,
|
||||||
),
|
sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||||
sorter: (a, b) => a.created - b.created,
|
},
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("audit.fields.useremail"),
|
title: t("audit.fields.useremail"),
|
||||||
dataIndex: "useremail",
|
dataIndex: "useremail",
|
||||||
key: "useremail",
|
key: "useremail",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
}
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const formItemLayout = {
|
const formItemLayout = {
|
||||||
labelCol: {
|
labelCol: {
|
||||||
xs: {span: 12},
|
xs: { span: 12 },
|
||||||
sm: {span: 5},
|
sm: { span: 5 }
|
||||||
},
|
},
|
||||||
wrapperCol: {
|
wrapperCol: {
|
||||||
xs: {span: 24},
|
xs: { span: 24 },
|
||||||
sm: {span: 12},
|
sm: { span: 12 }
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {List} from "antd";
|
import { List } from "antd";
|
||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import {FaArrowRight} from "react-icons/fa";
|
import { FaArrowRight } from "react-icons/fa";
|
||||||
|
|
||||||
export default function AuditTrailValuesComponent({oldV, newV}) {
|
export default function AuditTrailValuesComponent({ oldV, newV }) {
|
||||||
if (!oldV && !newV) return <div></div>;
|
if (!oldV && !newV) return <div></div>;
|
||||||
|
|
||||||
if (!oldV && newV)
|
|
||||||
return (
|
|
||||||
<List style={{width: "800px"}} bordered size='small'>
|
|
||||||
{Object.keys(newV).map((key, idx) => (
|
|
||||||
<List.Item key={idx} value={key}>
|
|
||||||
{key}: {JSON.stringify(newV[key])}
|
|
||||||
</List.Item>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (!oldV && newV)
|
||||||
return (
|
return (
|
||||||
<List style={{width: "800px"}} bordered size='small'>
|
<List style={{ width: "800px" }} bordered size="small">
|
||||||
{Object.keys(oldV).map((key, idx) => (
|
{Object.keys(newV).map((key, idx) => (
|
||||||
<List.Item key={idx}>
|
<List.Item key={idx} value={key}>
|
||||||
{key}: {oldV[key]} <Icon component={FaArrowRight}/>
|
{key}: {JSON.stringify(newV[key])}
|
||||||
{JSON.stringify(newV[key])}
|
</List.Item>
|
||||||
</List.Item>
|
))}
|
||||||
))}
|
</List>
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List style={{ width: "800px" }} bordered size="small">
|
||||||
|
{Object.keys(oldV).map((key, idx) => (
|
||||||
|
<List.Item key={idx}>
|
||||||
|
{key}: {oldV[key]} <Icon component={FaArrowRight} />
|
||||||
|
{JSON.stringify(newV[key])}
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import {Popover, Tag} from "antd";
|
import { Popover, Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Barcode from "react-barcode";
|
import Barcode from "react-barcode";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function BarcodePopupComponent({value, children}) {
|
export default function BarcodePopupComponent({ value, children }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Popover
|
<Popover content={<Barcode value={value || ""} background="transparent" displayValue={false} />}>
|
||||||
content={
|
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||||
<Barcode
|
</Popover>
|
||||||
value={value || ""}
|
</div>
|
||||||
background="transparent"
|
);
|
||||||
displayValue={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,136 +1,128 @@
|
|||||||
import {Checkbox, Form, Skeleton, Typography} from "antd";
|
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||||
import React, {useEffect} from "react";
|
import React, { useEffect } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
import "./bill-cm-returns-table.styles.scss";
|
import "./bill-cm-returns-table.styles.scss";
|
||||||
|
|
||||||
export default function BillCmdReturnsTableComponent({
|
export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
|
||||||
form,
|
const { t } = useTranslation();
|
||||||
returnLoading,
|
|
||||||
returnData,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (returnData) {
|
if (returnData) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
outstanding_returns: returnData.parts_order_lines,
|
outstanding_returns: returnData.parts_order_lines
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}, [returnData, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prev, cur) =>
|
||||||
|
prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid
|
||||||
|
}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const isReturn = form.getFieldValue("is_credit_memo");
|
||||||
|
|
||||||
|
if (!isReturn) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}, [returnData, form]);
|
|
||||||
|
|
||||||
return (
|
if (returnLoading) return <Skeleton />;
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(prev, cur) =>
|
|
||||||
prev.jobid !== cur.jobid ||
|
|
||||||
prev.is_credit_memo !== cur.is_credit_memo ||
|
|
||||||
prev.vendorid !== cur.vendorid
|
|
||||||
}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const isReturn = form.getFieldValue("is_credit_memo");
|
|
||||||
|
|
||||||
if (!isReturn) {
|
return (
|
||||||
return null;
|
<Form.List name="outstanding_returns">
|
||||||
}
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={4}>{t("bills.labels.creditsnotreceived")}</Typography.Title>
|
||||||
|
<table className="bill-cm-returns-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("parts_orders.fields.line_desc")}</th>
|
||||||
|
<th>{t("parts_orders.fields.part_type")}</th>
|
||||||
|
<th>{t("parts_orders.fields.quantity")}</th>
|
||||||
|
<th>{t("parts_orders.fields.act_price")}</th>
|
||||||
|
<th>{t("parts_orders.fields.cost")}</th>
|
||||||
|
<th>{t("parts_orders.labels.mark_as_received")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<tr key={field.key}>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
// label={t("joblines.fields.line_desc")}
|
||||||
|
key={`${index}line_desc`}
|
||||||
|
name={[field.name, "line_desc"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
if (returnLoading) return <Skeleton/>;
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}part_type`}
|
||||||
|
name={[field.name, "part_type"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}quantity`}
|
||||||
|
name={[field.name, "quantity"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}act_price`}
|
||||||
|
name={[field.name, "act_price"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}cost`}
|
||||||
|
name={[field.name, "cost"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
return (
|
<td>
|
||||||
<Form.List name="outstanding_returns">
|
<Form.Item
|
||||||
{(fields, {add, remove, move}) => {
|
span={2}
|
||||||
return (
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
<>
|
key={`${index}cm_received`}
|
||||||
<Typography.Title level={4}>
|
name={[field.name, "cm_received"]}
|
||||||
{t("bills.labels.creditsnotreceived")}
|
valuePropName="checked"
|
||||||
</Typography.Title>
|
>
|
||||||
<table className="bill-cm-returns-table">
|
<Checkbox />
|
||||||
<thead>
|
</Form.Item>
|
||||||
<tr>
|
</td>
|
||||||
<th>{t("parts_orders.fields.line_desc")}</th>
|
</tr>
|
||||||
<th>{t("parts_orders.fields.part_type")}</th>
|
))}
|
||||||
<th>{t("parts_orders.fields.quantity")}</th>
|
</tbody>
|
||||||
<th>{t("parts_orders.fields.act_price")}</th>
|
</table>
|
||||||
<th>{t("parts_orders.fields.cost")}</th>
|
</>
|
||||||
<th>{t("parts_orders.labels.mark_as_received")}</th>
|
);
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<tr key={field.key}>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
// label={t("joblines.fields.line_desc")}
|
|
||||||
key={`${index}line_desc`}
|
|
||||||
name={[field.name, "line_desc"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}part_type`}
|
|
||||||
name={[field.name, "part_type"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}quantity`}
|
|
||||||
name={[field.name, "quantity"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}act_price`}
|
|
||||||
name={[field.name, "act_price"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}cost`}
|
|
||||||
name={[field.name, "cost"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}cm_received`}
|
|
||||||
name={[field.name, "cm_received"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Checkbox/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.List>
|
||||||
);
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,88 @@
|
|||||||
import {DeleteFilled} from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {Button, notification, Popconfirm} from "antd";
|
import { Button, notification, Popconfirm } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {DELETE_BILL} from "../../graphql/bills.queries";
|
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||||
|
|
||||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [deleteBill] = useMutation(DELETE_BILL);
|
const [deleteBill] = useMutation(DELETE_BILL);
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await deleteBill({
|
const result = await deleteBill({
|
||||||
variables: {billId: bill.id},
|
variables: { billId: bill.id },
|
||||||
update(cache, {errors}) {
|
update(cache, { errors }) {
|
||||||
if (errors) return;
|
if (errors) return;
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
bills(existingBills, {readField}) {
|
bills(existingBills, { readField }) {
|
||||||
return existingBills.filter(
|
return existingBills.filter((billref) => bill.id !== readField("id", billref));
|
||||||
(billref) => bill.id !== readField("id", billref)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
search_bills(existingBills, {readField}) {
|
|
||||||
return existingBills.filter(
|
|
||||||
(billref) => bill.id !== readField("id", billref)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
search_bills(existingBills, { readField }) {
|
||||||
|
return existingBills.filter((billref) => bill.id !== readField("id", billref));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("bills.successes.deleted") });
|
notification["success"]({ message: t("bills.successes.deleted") });
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||||
type: "billdeleted",
|
type: "billdeleted"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (callback && typeof callback === "function") callback(bill.id);
|
if (callback && typeof callback === "function") callback(bill.id);
|
||||||
} else {
|
} else {
|
||||||
//Check if it's an fkey violation.
|
//Check if it's an fkey violation.
|
||||||
const error = JSON.stringify(result.errors);
|
const error = JSON.stringify(result.errors);
|
||||||
|
|
||||||
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("bills.errors.deleting", {
|
message: t("bills.errors.deleting", {
|
||||||
error: t("bills.errors.existinginventoryline"),
|
error: t("bills.errors.existinginventoryline")
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("bills.errors.deleting", {
|
message: t("bills.errors.deleting", {
|
||||||
error: JSON.stringify(result.errors),
|
error: JSON.stringify(result.errors)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="bills:delete" noauth={<></>}>
|
<RbacWrapper action="bills:delete" noauth={<></>}>
|
||||||
<Popconfirm
|
<Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
|
||||||
disabled={bill.exported}
|
<Button
|
||||||
onConfirm={handleDelete}
|
disabled={bill.exported}
|
||||||
title={t("bills.labels.deleteconfirm")}
|
// onClick={handleDelete}
|
||||||
>
|
loading={loading}
|
||||||
<Button
|
>
|
||||||
disabled={bill.exported}
|
<DeleteFilled />
|
||||||
// onClick={handleDelete}
|
</Button>
|
||||||
loading={loading}
|
</Popconfirm>
|
||||||
>
|
</RbacWrapper>
|
||||||
<DeleteFilled/>
|
);
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</RbacWrapper>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {useMutation, useQuery} from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import {Button, Divider, Form, Popconfirm, Space} from "antd";
|
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {useLocation} from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE} from "../../graphql/bill-lines.queries";
|
import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE } from "../../graphql/bill-lines.queries";
|
||||||
import {QUERY_BILL_BY_PK, UPDATE_BILL} from "../../graphql/bills.queries";
|
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
@@ -22,227 +22,212 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
|||||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||||
import {PageHeader} from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
insertAuditTrail: ({jobid, operation, type}) =>
|
|
||||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillDetailEditcontainer);
|
|
||||||
|
|
||||||
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
|
export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [updateLoading, setUpdateLoading] = useState(false);
|
const [updateLoading, setUpdateLoading] = useState(false);
|
||||||
const [update_bill] = useMutation(UPDATE_BILL);
|
const [update_bill] = useMutation(UPDATE_BILL);
|
||||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||||
|
|
||||||
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
|
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
||||||
variables: {billid: search.billid},
|
variables: { billid: search.billid },
|
||||||
skip: !!!search.billid,
|
skip: !!!search.billid,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
// ... rest of the code remains the same
|
// ... rest of the code remains the same
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
//It's got a previously deducted bill line!
|
//It's got a previously deducted bill line!
|
||||||
if (
|
if (
|
||||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0
|
||||||
0
|
)
|
||||||
)
|
setOpen(true);
|
||||||
setOpen(true);
|
else {
|
||||||
else {
|
form.submit();
|
||||||
form.submit();
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setUpdateLoading(true);
|
setUpdateLoading(true);
|
||||||
//let adjustmentsToInsert = {};
|
//let adjustmentsToInsert = {};
|
||||||
|
|
||||||
const {billlines, upload, ...bill} = values;
|
const { billlines, upload, ...bill } = values;
|
||||||
const updates = [];
|
const updates = [];
|
||||||
updates.push(
|
updates.push(
|
||||||
update_bill({
|
update_bill({
|
||||||
variables: {billId: search.billid, bill: bill},
|
variables: { billId: search.billid, bill: bill }
|
||||||
})
|
})
|
||||||
);
|
|
||||||
|
|
||||||
billlines.forEach((l) => {
|
|
||||||
delete l.selected;
|
|
||||||
});
|
|
||||||
|
|
||||||
//Find bill lines that were deleted.
|
|
||||||
const deletedJobLines = [];
|
|
||||||
|
|
||||||
data.bills_by_pk.billlines.forEach((a) => {
|
|
||||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
|
||||||
if (!matchingRecord) {
|
|
||||||
deletedJobLines.push(a);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
deletedJobLines.forEach((d) => {
|
|
||||||
updates.push(deleteBillLine({variables: {id: d.id}}));
|
|
||||||
});
|
|
||||||
|
|
||||||
billlines.forEach((billline) => {
|
|
||||||
const {deductedfromlbr, inventories, jobline, ...il} = billline;
|
|
||||||
delete il.__typename;
|
|
||||||
|
|
||||||
if (il.id) {
|
|
||||||
updates.push(
|
|
||||||
updateBillLine({
|
|
||||||
variables: {
|
|
||||||
billLineId: il.id,
|
|
||||||
billLine: {
|
|
||||||
...il,
|
|
||||||
deductedfromlbr: deductedfromlbr,
|
|
||||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
//It's a new line, have to insert it.
|
|
||||||
updates.push(
|
|
||||||
insertBillLine({
|
|
||||||
variables: {
|
|
||||||
billLines: [
|
|
||||||
{
|
|
||||||
...il,
|
|
||||||
deductedfromlbr: deductedfromlbr,
|
|
||||||
billid: search.billid,
|
|
||||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(updates);
|
|
||||||
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: bill.jobid,
|
|
||||||
billid: search.billid,
|
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
|
||||||
type: "billupdated",
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
form.setFieldsValue(transformData(data));
|
|
||||||
form.resetFields();
|
|
||||||
setOpen(false);
|
|
||||||
setUpdateLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
|
||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{loading && <LoadingSkeleton/>}
|
|
||||||
{data && (
|
|
||||||
<>
|
|
||||||
<PageHeader
|
|
||||||
title={
|
|
||||||
data &&
|
|
||||||
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<BillDetailEditReturn data={data}/>
|
|
||||||
<BillPrintButton billid={search.billid}/>
|
|
||||||
<Popconfirm
|
|
||||||
open={open}
|
|
||||||
onConfirm={() => form.submit()}
|
|
||||||
onCancel={() => setOpen(false)}
|
|
||||||
okButtonProps={{loading: updateLoading}}
|
|
||||||
title={t("bills.labels.editadjwarning")}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
htmlType="submit"
|
|
||||||
disabled={exported}
|
|
||||||
onClick={handleSave}
|
|
||||||
loading={updateLoading}
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
{t("general.actions.save")}
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
<BillReeportButtonComponent bill={data && data.bills_by_pk}/>
|
|
||||||
<BillMarkExportedButton bill={data && data.bills_by_pk}/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<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 ? (
|
|
||||||
<JobsDocumentsLocalGallery
|
|
||||||
job={{id: data ? data.bills_by_pk.jobid : null}}
|
|
||||||
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
|
||||||
vendorid={data ? data.bills_by_pk.vendorid : null}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<JobDocumentsGallery
|
|
||||||
jobId={data ? data.bills_by_pk.jobid : null}
|
|
||||||
billId={search.billid}
|
|
||||||
documentsList={data ? data.bills_by_pk.documents : []}
|
|
||||||
billsCallback={refetch}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
billlines.forEach((l) => {
|
||||||
|
delete l.selected;
|
||||||
|
});
|
||||||
|
|
||||||
|
//Find bill lines that were deleted.
|
||||||
|
const deletedJobLines = [];
|
||||||
|
|
||||||
|
data.bills_by_pk.billlines.forEach((a) => {
|
||||||
|
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||||
|
if (!matchingRecord) {
|
||||||
|
deletedJobLines.push(a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deletedJobLines.forEach((d) => {
|
||||||
|
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
billlines.forEach((billline) => {
|
||||||
|
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||||
|
delete il.__typename;
|
||||||
|
|
||||||
|
if (il.id) {
|
||||||
|
updates.push(
|
||||||
|
updateBillLine({
|
||||||
|
variables: {
|
||||||
|
billLineId: il.id,
|
||||||
|
billLine: {
|
||||||
|
...il,
|
||||||
|
deductedfromlbr: deductedfromlbr,
|
||||||
|
joblineid: il.joblineid === "noline" ? null : il.joblineid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
//It's a new line, have to insert it.
|
||||||
|
updates.push(
|
||||||
|
insertBillLine({
|
||||||
|
variables: {
|
||||||
|
billLines: [
|
||||||
|
{
|
||||||
|
...il,
|
||||||
|
deductedfromlbr: deductedfromlbr,
|
||||||
|
billid: search.billid,
|
||||||
|
joblineid: il.joblineid === "noline" ? null : il.joblineid
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(updates);
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: bill.jobid,
|
||||||
|
billid: search.billid,
|
||||||
|
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||||
|
type: "billupdated"
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
form.setFieldsValue(transformData(data));
|
||||||
|
form.resetFields();
|
||||||
|
setOpen(false);
|
||||||
|
setUpdateLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||||
|
|
||||||
|
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading && <LoadingSkeleton />}
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title={data && `${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<BillDetailEditReturn data={data} />
|
||||||
|
<BillPrintButton billid={search.billid} />
|
||||||
|
<Popconfirm
|
||||||
|
open={open}
|
||||||
|
onConfirm={() => form.submit()}
|
||||||
|
onCancel={() => setOpen(false)}
|
||||||
|
okButtonProps={{ loading: updateLoading }}
|
||||||
|
title={t("bills.labels.editadjwarning")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
htmlType="submit"
|
||||||
|
disabled={exported}
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={updateLoading}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
||||||
|
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<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 ? (
|
||||||
|
<JobsDocumentsLocalGallery
|
||||||
|
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||||
|
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||||
|
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<JobDocumentsGallery
|
||||||
|
jobId={data ? data.bills_by_pk.jobid : null}
|
||||||
|
billId={search.billid}
|
||||||
|
documentsList={data ? data.bills_by_pk.documents : []}
|
||||||
|
billsCallback={refetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformData = (data) => {
|
const transformData = (data) => {
|
||||||
return data
|
return data
|
||||||
? {
|
? {
|
||||||
...data.bills_by_pk,
|
...data.bills_by_pk,
|
||||||
|
|
||||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||||
applicable_taxes: {
|
applicable_taxes: {
|
||||||
federal:
|
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||||
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
local: (i.applicable_taxes && i.applicable_taxes.local) || false
|
||||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
}
|
||||||
},
|
};
|
||||||
};
|
}),
|
||||||
}),
|
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null
|
||||||
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
|
}
|
||||||
}
|
: {};
|
||||||
: {};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,189 +1,168 @@
|
|||||||
import {Button, Checkbox, Form, Modal} from "antd";
|
import { Button, Checkbox, Form, Modal } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
insertAuditTrail: ({jobid, operation, type}) =>
|
|
||||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillDetailEditReturn);
|
|
||||||
|
|
||||||
export function BillDetailEditReturn({
|
export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
|
||||||
setPartsOrderContext,
|
const search = queryString.parse(useLocation().search);
|
||||||
insertAuditTrail,
|
const history = useNavigate();
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
data,
|
const [form] = Form.useForm();
|
||||||
disabled,
|
const [open, setOpen] = useState(false);
|
||||||
}) {
|
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
const history = useNavigate();
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleFinish = ({billlines}) => {
|
const handleFinish = ({ billlines }) => {
|
||||||
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
||||||
|
|
||||||
setPartsOrderContext({
|
setPartsOrderContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {
|
context: {
|
||||||
jobId: data.bills_by_pk.jobid,
|
jobId: data.bills_by_pk.jobid,
|
||||||
vendorId: data.bills_by_pk.vendorid,
|
vendorId: data.bills_by_pk.vendorid,
|
||||||
returnFromBill: data.bills_by_pk.id,
|
returnFromBill: data.bills_by_pk.id,
|
||||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||||
linesToOrder: data.bills_by_pk.billlines
|
linesToOrder: data.bills_by_pk.billlines
|
||||||
.filter((l) => selectedLines.includes(l.id))
|
.filter((l) => selectedLines.includes(l.id))
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
return {
|
return {
|
||||||
line_desc: i.line_desc,
|
line_desc: i.line_desc,
|
||||||
// db_price: i.actual_price,
|
// db_price: i.actual_price,
|
||||||
act_price: i.actual_price,
|
act_price: i.actual_price,
|
||||||
cost: i.actual_cost,
|
cost: i.actual_cost,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
joblineid: i.joblineid,
|
joblineid: i.joblineid,
|
||||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||||
part_type: i.jobline && i.jobline.part_type,
|
part_type: i.jobline && i.jobline.part_type
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
isReturn: true,
|
isReturn: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
delete search.billid;
|
delete search.billid;
|
||||||
|
|
||||||
history({search: queryString.stringify(search)});
|
history({ search: queryString.stringify(search) });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open === false) form.resetFields();
|
if (open === false) form.resetFields();
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
>
|
>
|
||||||
<Form
|
<Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
|
||||||
initialValues={data && data.bills_by_pk}
|
<Form.List name={["billlines"]}>
|
||||||
onFinish={handleFinish}
|
{(fields, { add, remove, move }) => {
|
||||||
form={form}
|
return (
|
||||||
>
|
<table style={{ tableLayout: "auto", width: "100%" }}>
|
||||||
<Form.List name={["billlines"]}>
|
<thead>
|
||||||
{(fields, {add, remove, move}) => {
|
<tr>
|
||||||
return (
|
<td>
|
||||||
<table style={{tableLayout: "auto", width: "100%"}}>
|
<Checkbox
|
||||||
<thead>
|
onChange={(e) => {
|
||||||
<tr>
|
form.setFieldsValue({
|
||||||
<td>
|
billlines: form.getFieldsValue().billlines.map((b) => ({
|
||||||
<Checkbox
|
...b,
|
||||||
onChange={(e) => {
|
selected: e.target.checked
|
||||||
form.setFieldsValue({
|
}))
|
||||||
billlines: form
|
});
|
||||||
.getFieldsValue()
|
}}
|
||||||
.billlines.map((b) => ({
|
/>
|
||||||
...b,
|
</td>
|
||||||
selected: e.target.checked,
|
<td>{t("billlines.fields.line_desc")}</td>
|
||||||
})),
|
<td>{t("billlines.fields.quantity")}</td>
|
||||||
});
|
<td>{t("billlines.fields.actual_price")}</td>
|
||||||
}}
|
<td>{t("billlines.fields.actual_cost")}</td>
|
||||||
/>
|
</tr>
|
||||||
</td>
|
</thead>
|
||||||
<td>{t("billlines.fields.line_desc")}</td>
|
<tbody>
|
||||||
<td>{t("billlines.fields.quantity")}</td>
|
{fields.map((field, index) => (
|
||||||
<td>{t("billlines.fields.actual_price")}</td>
|
<tr key={field.key}>
|
||||||
<td>{t("billlines.fields.actual_cost")}</td>
|
<td>
|
||||||
</tr>
|
<Form.Item
|
||||||
</thead>
|
// label={t("joblines.fields.selected")}
|
||||||
<tbody>
|
key={`${index}selected`}
|
||||||
{fields.map((field, index) => (
|
name={[field.name, "selected"]}
|
||||||
<tr key={field.key}>
|
valuePropName="checked"
|
||||||
<td>
|
>
|
||||||
<Form.Item
|
<Checkbox />
|
||||||
// label={t("joblines.fields.selected")}
|
</Form.Item>
|
||||||
key={`${index}selected`}
|
</td>
|
||||||
name={[field.name, "selected"]}
|
<td>
|
||||||
valuePropName="checked"
|
<Form.Item
|
||||||
>
|
// label={t("joblines.fields.line_desc")}
|
||||||
<Checkbox/>
|
key={`${index}line_desc`}
|
||||||
</Form.Item>
|
name={[field.name, "line_desc"]}
|
||||||
</td>
|
>
|
||||||
<td>
|
<ReadOnlyFormItemComponent />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
// label={t("joblines.fields.line_desc")}
|
</td>
|
||||||
key={`${index}line_desc`}
|
<td>
|
||||||
name={[field.name, "line_desc"]}
|
<Form.Item
|
||||||
>
|
// label={t("joblines.fields.quantity")}
|
||||||
<ReadOnlyFormItemComponent/>
|
key={`${index}quantity`}
|
||||||
</Form.Item>
|
name={[field.name, "quantity"]}
|
||||||
</td>
|
>
|
||||||
<td>
|
<ReadOnlyFormItemComponent />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
// label={t("joblines.fields.quantity")}
|
</td>
|
||||||
key={`${index}quantity`}
|
<td>
|
||||||
name={[field.name, "quantity"]}
|
<Form.Item
|
||||||
>
|
// label={t("joblines.fields.actual_price")}
|
||||||
<ReadOnlyFormItemComponent/>
|
key={`${index}actual_price`}
|
||||||
</Form.Item>
|
name={[field.name, "actual_price"]}
|
||||||
</td>
|
>
|
||||||
<td>
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
// label={t("joblines.fields.actual_price")}
|
</td>
|
||||||
key={`${index}actual_price`}
|
<td>
|
||||||
name={[field.name, "actual_price"]}
|
<Form.Item
|
||||||
>
|
// label={t("joblines.fields.actual_cost")}
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
key={`${index}actual_cost`}
|
||||||
</Form.Item>
|
name={[field.name, "actual_cost"]}
|
||||||
</td>
|
>
|
||||||
<td>
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
// label={t("joblines.fields.actual_cost")}
|
</td>
|
||||||
key={`${index}actual_cost`}
|
</tr>
|
||||||
name={[field.name, "actual_cost"]}
|
))}
|
||||||
>
|
</tbody>
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
</table>
|
||||||
</Form.Item>
|
);
|
||||||
</td>
|
}}
|
||||||
</tr>
|
</Form.List>
|
||||||
))}
|
</Form>
|
||||||
</tbody>
|
</Modal>
|
||||||
</table>
|
<Button
|
||||||
);
|
disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
|
||||||
}}
|
onClick={() => {
|
||||||
</Form.List>
|
setOpen(true);
|
||||||
</Form>
|
}}
|
||||||
</Modal>
|
>
|
||||||
<Button
|
{t("bills.actions.return")}
|
||||||
disabled={
|
</Button>
|
||||||
data.bills_by_pk.is_credit_memo ||
|
</>
|
||||||
data.bills_by_pk.isinhouse ||
|
);
|
||||||
disabled
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("bills.actions.return")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
import {Drawer, Grid} from "antd";
|
import { Drawer, Grid } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import BillDetailEditComponent from "./bill-detail-edit-component";
|
import BillDetailEditComponent from "./bill-detail-edit-component";
|
||||||
|
|
||||||
export default function BillDetailEditcontainer() {
|
export default function BillDetailEditcontainer() {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
.slice(-1)[0];
|
.slice(-1)[0];
|
||||||
|
|
||||||
const bpoints = {
|
const bpoints = {
|
||||||
xs: "100%",
|
xs: "100%",
|
||||||
sm: "100%",
|
sm: "100%",
|
||||||
md: "100%",
|
md: "100%",
|
||||||
lg: "100%",
|
lg: "100%",
|
||||||
xl: "90%",
|
xl: "90%",
|
||||||
xxl: "90%",
|
xxl: "90%"
|
||||||
};
|
};
|
||||||
const drawerPercentage = selectedBreakpoint
|
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||||
? bpoints[selectedBreakpoint[0]]
|
|
||||||
: "100%";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width={drawerPercentage}
|
width={drawerPercentage}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
delete search.billid;
|
delete search.billid;
|
||||||
history({search: queryString.stringify(search)});
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
open={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent/>
|
<BillDetailEditComponent />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,471 +1,426 @@
|
|||||||
import {useApolloClient, useMutation} from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import {Button, Checkbox, Form, Modal, notification, Space} from "antd";
|
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, {useEffect, useMemo, useState} from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {INSERT_NEW_BILL} from "../../graphql/bills.queries";
|
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||||
import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries";
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries";
|
import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries";
|
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import {TemplateList} from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import confirmDialog from "../../utils/asyncConfirm";
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility";
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
import {handleUpload} from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||||
insertAuditTrail: ({jobid, billid, operation, type}) =>
|
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
||||||
dispatch(insertAuditTrail({jobid, billid, operation, type })),
|
dispatch(insertAuditTrail({ jobid, billid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
const Templates = TemplateList("job_special");
|
const Templates = TemplateList("job_special");
|
||||||
|
|
||||||
function BillEnterModalContainer({
|
function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
|
||||||
billEnterModal,
|
const [form] = Form.useForm();
|
||||||
toggleModalVisible,
|
const { t } = useTranslation();
|
||||||
bodyshop,
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
currentUser,
|
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||||
insertAuditTrail,
|
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||||
}) {
|
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||||
const [form] = Form.useForm();
|
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||||
const {t} = useTranslation();
|
const [loading, setLoading] = useState(false);
|
||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const client = useApolloClient();
|
||||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
|
||||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
|
||||||
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
|
||||||
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [generateLabel, setGenerateLabel] = useLocalStorage(
|
|
||||||
"enter_bill_generate_label",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
|
const {
|
||||||
attributes: {},
|
treatments: { Enhanced_Payroll }
|
||||||
names: ["Enhanced_Payroll"],
|
} = useSplitTreatments({
|
||||||
splitKey: bodyshop.imexshopid,
|
attributes: {},
|
||||||
|
names: ["Enhanced_Payroll"],
|
||||||
|
splitKey: bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...billEnterModal.context.bill,
|
||||||
|
//Added as a part of IO-2436 for capturing parts price changes.
|
||||||
|
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
|
||||||
|
...line,
|
||||||
|
original_actual_price: line.actual_price
|
||||||
|
})),
|
||||||
|
jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||||
|
federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0,
|
||||||
|
state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0,
|
||||||
|
local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0
|
||||||
|
};
|
||||||
|
}, [billEnterModal, bodyshop]);
|
||||||
|
|
||||||
|
const handleFinish = async (values) => {
|
||||||
|
let totals = CalculateBillTotal(values);
|
||||||
|
if (totals.discrepancy.getAmount() !== 0) {
|
||||||
|
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
|
||||||
|
|
||||||
|
let adjustmentsToInsert = {};
|
||||||
|
let payrollAdjustmentsToInsert = [];
|
||||||
|
|
||||||
|
const r1 = await insertBill({
|
||||||
|
variables: {
|
||||||
|
bill: [
|
||||||
|
{
|
||||||
|
...remainingValues,
|
||||||
|
billlines: {
|
||||||
|
data:
|
||||||
|
remainingValues.billlines &&
|
||||||
|
remainingValues.billlines.map((i) => {
|
||||||
|
const {
|
||||||
|
deductedfromlbr,
|
||||||
|
lbr_adjustment,
|
||||||
|
location: lineLocation,
|
||||||
|
part_type,
|
||||||
|
create_ppc,
|
||||||
|
original_actual_price,
|
||||||
|
...restI
|
||||||
|
} = i;
|
||||||
|
|
||||||
|
if (Enhanced_Payroll.treatment === "on") {
|
||||||
|
if (
|
||||||
|
deductedfromlbr &&
|
||||||
|
true //payroll is on
|
||||||
|
) {
|
||||||
|
payrollAdjustmentsToInsert.push({
|
||||||
|
id: i.joblineid,
|
||||||
|
convertedtolbr: true,
|
||||||
|
convertedtolbr_data: {
|
||||||
|
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
|
||||||
|
mod_lbr_ty: lbr_adjustment.mod_lbr_ty
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (deductedfromlbr) {
|
||||||
|
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
|
||||||
|
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
|
||||||
|
restI.actual_price / lbr_adjustment.rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...restI,
|
||||||
|
deductedfromlbr: deductedfromlbr,
|
||||||
|
lbr_adjustment,
|
||||||
|
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||||
|
applicable_taxes: {
|
||||||
|
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||||
|
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||||
|
local: (i.applicable_taxes && i.applicable_taxes.local) || false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
||||||
|
awaitRefetchQueries: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const formValues = useMemo(() => {
|
await Promise.all(
|
||||||
return {
|
payrollAdjustmentsToInsert.map((li) => {
|
||||||
...billEnterModal.context.bill,
|
return updateJobLines({
|
||||||
//Added as a part of IO-2436 for capturing parts price changes.
|
variables: {
|
||||||
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
|
lineId: li.id,
|
||||||
...line,
|
line: {
|
||||||
original_actual_price: line.actual_price,
|
convertedtolbr: li.convertedtolbr,
|
||||||
})),
|
convertedtolbr_data: li.convertedtolbr_data
|
||||||
jobid:
|
|
||||||
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
|
||||||
federal_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
|
||||||
0,
|
|
||||||
state_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
|
||||||
0,
|
|
||||||
local_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
|
||||||
0,
|
|
||||||
};
|
|
||||||
}, [billEnterModal, bodyshop]);
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
let totals = CalculateBillTotal(values);
|
|
||||||
if (totals.discrepancy.getAmount() !== 0) {
|
|
||||||
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
const {
|
|
||||||
upload,
|
|
||||||
location,
|
|
||||||
outstanding_returns,
|
|
||||||
inventory,
|
|
||||||
federal_tax_exempt,
|
|
||||||
...remainingValues
|
|
||||||
} = values;
|
|
||||||
|
|
||||||
let adjustmentsToInsert = {};
|
|
||||||
let payrollAdjustmentsToInsert = [];
|
|
||||||
|
|
||||||
const r1 = await insertBill({
|
|
||||||
variables: {
|
|
||||||
bill: [
|
|
||||||
{
|
|
||||||
...remainingValues,
|
|
||||||
billlines: {
|
|
||||||
data:
|
|
||||||
remainingValues.billlines &&
|
|
||||||
remainingValues.billlines.map((i) => {
|
|
||||||
const {
|
|
||||||
deductedfromlbr,
|
|
||||||
lbr_adjustment,
|
|
||||||
location: lineLocation,
|
|
||||||
part_type,
|
|
||||||
create_ppc,
|
|
||||||
original_actual_price,
|
|
||||||
...restI
|
|
||||||
} = i;
|
|
||||||
|
|
||||||
if (Enhanced_Payroll.treatment === "on") {
|
|
||||||
if (
|
|
||||||
deductedfromlbr &&
|
|
||||||
true //payroll is on
|
|
||||||
) {
|
|
||||||
payrollAdjustmentsToInsert.push({
|
|
||||||
id: i.joblineid,
|
|
||||||
convertedtolbr: true,
|
|
||||||
convertedtolbr_data: {
|
|
||||||
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
|
|
||||||
mod_lbr_ty: lbr_adjustment.mod_lbr_ty,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (deductedfromlbr) {
|
|
||||||
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
|
|
||||||
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
|
|
||||||
restI.actual_price / lbr_adjustment.rate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...restI,
|
|
||||||
deductedfromlbr: deductedfromlbr,
|
|
||||||
lbr_adjustment,
|
|
||||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
|
||||||
applicable_taxes: {
|
|
||||||
federal:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
|
||||||
false,
|
|
||||||
state:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
|
||||||
false,
|
|
||||||
local:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
|
||||||
awaitRefetchQueries: true
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
payrollAdjustmentsToInsert.map((li) => {
|
if (adjKeys.length > 0) {
|
||||||
return updateJobLines({
|
//Query the adjustments, merge, and update them.
|
||||||
variables: {
|
const existingAdjustments = await client.query({
|
||||||
lineId: li.id,
|
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
line: {
|
variables: {
|
||||||
convertedtolbr: li.convertedtolbr,
|
id: values.jobid
|
||||||
convertedtolbr_data: li.convertedtolbr_data,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
|
||||||
if (adjKeys.length > 0) {
|
|
||||||
//Query the adjustments, merge, and update them.
|
|
||||||
const existingAdjustments = await client.query({
|
|
||||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
|
||||||
variables: {
|
|
||||||
id: values.jobid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const newAdjustments = _.cloneDeep(
|
|
||||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
|
||||||
);
|
|
||||||
|
|
||||||
adjKeys.forEach((key) => {
|
|
||||||
newAdjustments[key] =
|
|
||||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
|
||||||
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: values.jobid,
|
|
||||||
operation: AuditTrailMapping.jobmodifylbradj({
|
|
||||||
mod_lbr_ty: key,
|
|
||||||
hours: adjustmentsToInsert[key].toFixed(1),
|
|
||||||
}),
|
|
||||||
type: "jobmodifylbradj",});
|
|
||||||
});
|
|
||||||
|
|
||||||
const jobUpdate = client.mutate({
|
|
||||||
mutation: UPDATE_JOB,
|
|
||||||
variables: {
|
|
||||||
jobId: values.jobid,
|
|
||||||
job: {lbr_adjustments: newAdjustments},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!jobUpdate.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.saving", {
|
|
||||||
message: JSON.stringify(jobUpdate.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const markPolReceived =
|
const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
|
||||||
outstanding_returns &&
|
|
||||||
outstanding_returns.filter((o) => o.cm_received === true);
|
|
||||||
|
|
||||||
if (markPolReceived && markPolReceived.length > 0) {
|
adjKeys.forEach((key) => {
|
||||||
const r2 = await updatePartsOrderLines({
|
newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||||
variables: {partsLineIds: markPolReceived.map((p) => p.id)},
|
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID" ],
|
|
||||||
});
|
|
||||||
if (!!r2.errors) {
|
|
||||||
setLoading(false);
|
|
||||||
setEnterAgain(false);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("parts_orders.errors.updating", {
|
|
||||||
message: JSON.stringify(r2.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!r1.errors) {
|
|
||||||
setLoading(false);
|
|
||||||
setEnterAgain(false);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("bills.errors.creating", {
|
|
||||||
message: JSON.stringify(r1.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const billId = r1.data.insert_bills.returning[0].id;
|
|
||||||
const markInventoryConsumed =
|
|
||||||
inventory && inventory.filter((i) => i.consumefrominventory);
|
|
||||||
|
|
||||||
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
|
||||||
const r2 = await updateInventoryLines({
|
|
||||||
variables: {
|
|
||||||
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
|
||||||
consumedbybillid: billId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!r2.errors) {
|
|
||||||
setLoading(false);
|
|
||||||
setEnterAgain(false);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("inventory.errors.updating", {
|
|
||||||
message: JSON.stringify(r2.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//If it's not a credit memo, update the statuses.
|
|
||||||
|
|
||||||
if (!values.is_credit_memo) {
|
|
||||||
await Promise.all(
|
|
||||||
remainingValues.billlines
|
|
||||||
.filter((il) => il.joblineid !== "noline")
|
|
||||||
.map((li) => {
|
|
||||||
return updateJobLines({
|
|
||||||
variables: {
|
|
||||||
lineId: li.joblineid,
|
|
||||||
line: {
|
|
||||||
location: li.location || location,
|
|
||||||
status:
|
|
||||||
bodyshop.md_order_statuses.default_received || "Received*",
|
|
||||||
//Added parts price changes.
|
|
||||||
...(li.create_ppc &&
|
|
||||||
li.original_actual_price !== li.actual_price
|
|
||||||
? {
|
|
||||||
act_price_before_ppc: li.original_actual_price,
|
|
||||||
act_price: li.actual_price,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////
|
|
||||||
if (upload && upload.length > 0) {
|
|
||||||
//insert Each of the documents?
|
|
||||||
|
|
||||||
if (bodyshop.uselocalmediaserver) {
|
|
||||||
upload.forEach((u) => {
|
|
||||||
handleLocalUpload({
|
|
||||||
ev: {file: u.originFileObj},
|
|
||||||
context: {
|
|
||||||
jobid: values.jobid,
|
|
||||||
invoice_number: remainingValues.invoice_number,
|
|
||||||
vendorid: remainingValues.vendorid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
upload.forEach((u) => {
|
|
||||||
handleUpload(
|
|
||||||
{file: u.originFileObj},
|
|
||||||
{
|
|
||||||
bodyshop: bodyshop,
|
|
||||||
uploaded_by: currentUser.email,
|
|
||||||
jobId: values.jobid,
|
|
||||||
billId: billId,
|
|
||||||
tagsArray: null,
|
|
||||||
callback: null,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
///////////////////////////
|
|
||||||
setLoading(false);
|
|
||||||
notification["success"]({
|
|
||||||
message: t("bills.successes.created"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (generateLabel) {
|
|
||||||
GenerateDocument(
|
|
||||||
{
|
|
||||||
name: Templates.parts_invoice_label_single.key,
|
|
||||||
variables: {
|
|
||||||
id: billId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
"p"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: values.jobid,
|
jobid: values.jobid,
|
||||||
billid: billId,
|
operation: AuditTrailMapping.jobmodifylbradj({
|
||||||
operation: AuditTrailMapping.billposted(
|
mod_lbr_ty: key,
|
||||||
r1.data.insert_bills.returning[0].invoice_number
|
hours: adjustmentsToInsert[key].toFixed(1)
|
||||||
),
|
}),
|
||||||
type: "billposted",
|
type: "jobmodifylbradj"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobUpdate = client.mutate({
|
||||||
|
mutation: UPDATE_JOB,
|
||||||
|
variables: {
|
||||||
|
jobId: values.jobid,
|
||||||
|
job: { lbr_adjustments: newAdjustments }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!!jobUpdate.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.saving", {
|
||||||
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true);
|
||||||
|
|
||||||
|
if (markPolReceived && markPolReceived.length > 0) {
|
||||||
|
const r2 = await updatePartsOrderLines({
|
||||||
|
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||||
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
||||||
|
});
|
||||||
|
if (!!r2.errors) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnterAgain(false);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("parts_orders.errors.updating", {
|
||||||
|
message: JSON.stringify(r2.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!r1.errors) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnterAgain(false);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.creating", {
|
||||||
|
message: JSON.stringify(r1.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const billId = r1.data.insert_bills.returning[0].id;
|
||||||
|
const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory);
|
||||||
|
|
||||||
|
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
||||||
|
const r2 = await updateInventoryLines({
|
||||||
|
variables: {
|
||||||
|
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
||||||
|
consumedbybillid: billId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!!r2.errors) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnterAgain(false);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("inventory.errors.updating", {
|
||||||
|
message: JSON.stringify(r2.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//If it's not a credit memo, update the statuses.
|
||||||
|
|
||||||
|
if (!values.is_credit_memo) {
|
||||||
|
await Promise.all(
|
||||||
|
remainingValues.billlines
|
||||||
|
.filter((il) => il.joblineid !== "noline")
|
||||||
|
.map((li) => {
|
||||||
|
return updateJobLines({
|
||||||
|
variables: {
|
||||||
|
lineId: li.joblineid,
|
||||||
|
line: {
|
||||||
|
location: li.location || location,
|
||||||
|
status: bodyshop.md_order_statuses.default_received || "Received*",
|
||||||
|
//Added parts price changes.
|
||||||
|
...(li.create_ppc && li.original_actual_price !== li.actual_price
|
||||||
|
? {
|
||||||
|
act_price_before_ppc: li.original_actual_price,
|
||||||
|
act_price: li.actual_price
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
if (upload && upload.length > 0) {
|
||||||
|
//insert Each of the documents?
|
||||||
|
|
||||||
|
if (bodyshop.uselocalmediaserver) {
|
||||||
|
upload.forEach((u) => {
|
||||||
|
handleLocalUpload({
|
||||||
|
ev: { file: u.originFileObj },
|
||||||
|
context: {
|
||||||
|
jobid: values.jobid,
|
||||||
|
invoice_number: remainingValues.invoice_number,
|
||||||
|
vendorid: remainingValues.vendorid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
upload.forEach((u) => {
|
||||||
|
handleUpload(
|
||||||
|
{ file: u.originFileObj },
|
||||||
|
{
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
jobId: values.jobid,
|
||||||
|
billId: billId,
|
||||||
|
tagsArray: null,
|
||||||
|
callback: null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///////////////////////////
|
||||||
|
setLoading(false);
|
||||||
|
notification["success"]({
|
||||||
|
message: t("bills.successes.created")
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enterAgain) {
|
if (generateLabel) {
|
||||||
form.resetFields();
|
GenerateDocument(
|
||||||
form.setFieldsValue({
|
{
|
||||||
...formValues,
|
name: Templates.parts_invoice_label_single.key,
|
||||||
vendorid:values.vendorid,
|
variables: {
|
||||||
billlines: [],
|
id: billId
|
||||||
});
|
}
|
||||||
// form.resetFields();
|
},
|
||||||
} else {
|
{},
|
||||||
toggleModalVisible();
|
"p"
|
||||||
}
|
);
|
||||||
setEnterAgain(false);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||||
const r = window.confirm(t("general.labels.cancel"));
|
|
||||||
if (r === true) {
|
|
||||||
toggleModalVisible();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
insertAuditTrail({
|
||||||
if (enterAgain) form.submit();
|
jobid: values.jobid,
|
||||||
}, [enterAgain, form]);
|
billid: billId,
|
||||||
|
operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
|
||||||
|
type: "billposted"
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (enterAgain) {
|
||||||
if (billEnterModal.open) {
|
form.resetFields();
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue({
|
||||||
} else {
|
...formValues,
|
||||||
form.resetFields();
|
vendorid: values.vendorid,
|
||||||
}
|
billlines: []
|
||||||
}, [billEnterModal.open, form, formValues]);
|
});
|
||||||
|
// form.resetFields();
|
||||||
|
} else {
|
||||||
|
toggleModalVisible();
|
||||||
|
}
|
||||||
|
setEnterAgain(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleCancel = () => {
|
||||||
<Modal
|
const r = window.confirm(t("general.labels.cancel"));
|
||||||
title={t("bills.labels.new")}
|
if (r === true) {
|
||||||
width={"98%"}
|
toggleModalVisible();
|
||||||
open={billEnterModal.open}
|
}
|
||||||
okText={t("general.actions.save")}
|
};
|
||||||
keyboard="false"
|
|
||||||
onOk={() => form.submit()}
|
useEffect(() => {
|
||||||
onCancel={handleCancel}
|
if (enterAgain) form.submit();
|
||||||
afterClose={() => {
|
}, [enterAgain, form]);
|
||||||
form.resetFields();
|
|
||||||
setLoading(false);
|
useEffect(() => {
|
||||||
}}
|
if (billEnterModal.open) {
|
||||||
footer={
|
form.setFieldsValue(formValues);
|
||||||
<Space>
|
} else {
|
||||||
<Checkbox
|
form.resetFields();
|
||||||
checked={generateLabel}
|
}
|
||||||
onChange={(e) => setGenerateLabel(e.target.checked)}
|
}, [billEnterModal.open, form, formValues]);
|
||||||
>
|
|
||||||
{t("bills.labels.generatepartslabel")}
|
return (
|
||||||
</Checkbox>
|
<Modal
|
||||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
title={t("bills.labels.new")}
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
width={"98%"}
|
||||||
{t("general.actions.save")}
|
open={billEnterModal.open}
|
||||||
</Button>
|
okText={t("general.actions.save")}
|
||||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
keyboard="false"
|
||||||
<Button
|
onOk={() => form.submit()}
|
||||||
type="primary"
|
onCancel={handleCancel}
|
||||||
loading={loading}
|
afterClose={() => {
|
||||||
onClick={() => {
|
form.resetFields();
|
||||||
setEnterAgain(true);
|
setLoading(false);
|
||||||
}}
|
}}
|
||||||
>
|
footer={
|
||||||
{t("general.actions.saveandnew")}
|
<Space>
|
||||||
</Button>
|
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
||||||
)}
|
{t("bills.labels.generatepartslabel")}
|
||||||
</Space>
|
</Checkbox>
|
||||||
}
|
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||||
destroyOnClose
|
<Button loading={loading} onClick={() => form.submit()}>
|
||||||
>
|
{t("general.actions.save")}
|
||||||
<Form
|
</Button>
|
||||||
onFinish={handleFinish}
|
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||||
autoComplete={"off"}
|
<Button
|
||||||
layout="vertical"
|
type="primary"
|
||||||
form={form}
|
loading={loading}
|
||||||
onFinishFailed={() => {
|
onClick={() => {
|
||||||
setEnterAgain(false);
|
setEnterAgain(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BillFormContainer
|
{t("general.actions.saveandnew")}
|
||||||
form={form}
|
</Button>
|
||||||
disableInvNumber={billEnterModal.context.disableInvNumber}
|
)}
|
||||||
/>
|
</Space>
|
||||||
</Form>
|
}
|
||||||
</Modal>
|
destroyOnClose
|
||||||
);
|
>
|
||||||
|
<Form
|
||||||
|
onFinish={handleFinish}
|
||||||
|
autoComplete={"off"}
|
||||||
|
layout="vertical"
|
||||||
|
form={form}
|
||||||
|
onFinishFailed={() => {
|
||||||
|
setEnterAgain(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillEnterModalContainer);
|
|
||||||
|
|||||||
@@ -1,136 +1,114 @@
|
|||||||
import {Form, Input, Table} from "antd";
|
import { Form, Input, Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {alphaSort} from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
|
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
|
||||||
|
|
||||||
export default function BillFormLinesExtended({
|
export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
|
||||||
lineData,
|
const [search, setSearch] = useState("");
|
||||||
discount,
|
const { t } = useTranslation();
|
||||||
form,
|
const columns = [
|
||||||
responsibilityCenters,
|
{
|
||||||
disabled,
|
title: t("joblines.fields.line_desc"),
|
||||||
}) {
|
dataIndex: "line_desc",
|
||||||
const [search, setSearch] = useState("");
|
key: "line_desc",
|
||||||
const {t} = useTranslation();
|
width: "10%",
|
||||||
const columns = [
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.oem_partno"),
|
||||||
|
dataIndex: "oem_partno",
|
||||||
|
key: "oem_partno",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.part_type"),
|
||||||
|
dataIndex: "part_type",
|
||||||
|
key: "part_type",
|
||||||
|
width: "10%",
|
||||||
|
filters: [
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.line_desc"),
|
text: t("jobs.labels.partsfilter"),
|
||||||
dataIndex: "line_desc",
|
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
|
||||||
key: "line_desc",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.oem_partno"),
|
text: t("joblines.fields.part_types.PAN"),
|
||||||
dataIndex: "oem_partno",
|
value: ["PAN", "PAP"]
|
||||||
key: "oem_partno",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.part_type"),
|
text: t("joblines.fields.part_types.PAL"),
|
||||||
dataIndex: "part_type",
|
value: ["PAL"]
|
||||||
key: "part_type",
|
|
||||||
width: "10%",
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
text: t("jobs.labels.partsfilter"),
|
|
||||||
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t("joblines.fields.part_types.PAN"),
|
|
||||||
value: ["PAN", "PAP"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t("joblines.fields.part_types.PAL"),
|
|
||||||
value: ["PAL"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t("joblines.fields.part_types.PAA"),
|
|
||||||
value: ["PAA"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: t("joblines.fields.part_types.PAS"),
|
|
||||||
value: ["PAS", "PASL"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onFilter: (value, record) => value.includes(record.part_type),
|
|
||||||
render: (text, record) =>
|
|
||||||
record.part_type
|
|
||||||
? t(`joblines.fields.part_types.${record.part_type}`)
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAA"),
|
||||||
|
value: ["PAA"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAS"),
|
||||||
|
value: ["PAS", "PASL"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onFilter: (value, record) => value.includes(record.part_type),
|
||||||
|
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.act_price"),
|
title: t("joblines.fields.act_price"),
|
||||||
dataIndex: "act_price",
|
dataIndex: "act_price",
|
||||||
key: "act_price",
|
key: "act_price",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
sorter: (a, b) => a.act_price - b.act_price,
|
sorter: (a, b) => a.act_price - b.act_price,
|
||||||
shouldCellUpdate: false,
|
shouldCellUpdate: false,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<>
|
<>
|
||||||
<CurrencyFormatter>
|
<CurrencyFormatter>
|
||||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
{record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
|
||||||
? record.prt_dsmk_m
|
</CurrencyFormatter>
|
||||||
: record.act_price}
|
{record.part_qty ? `(x ${record.part_qty})` : null}
|
||||||
</CurrencyFormatter>
|
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||||
{record.part_qty ? `(x ${record.part_qty})` : null}
|
<span style={{ marginLeft: ".2rem" }}>{`(${record.prt_dsmk_p}%)`}</span>
|
||||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
) : (
|
||||||
<span
|
<></>
|
||||||
style={{marginLeft: ".2rem"}}
|
)}
|
||||||
>{`(${record.prt_dsmk_p}%)`}</span>
|
</>
|
||||||
) : (
|
)
|
||||||
<></>
|
},
|
||||||
)}
|
{
|
||||||
</>
|
title: t("billlines.fields.posting"),
|
||||||
),
|
dataIndex: "posting",
|
||||||
},
|
key: "posting",
|
||||||
{
|
|
||||||
title: t("billlines.fields.posting"),
|
|
||||||
dataIndex: "posting",
|
|
||||||
key: "posting",
|
|
||||||
|
|
||||||
render: (text, record, index) => (
|
render: (text, record, index) => (
|
||||||
<Form.Item noStyle name={["billlineskeys", record.id]}>
|
<Form.Item noStyle name={["billlineskeys", record.id]}>
|
||||||
<BillFormItemsExtendedFormItem
|
<BillFormItemsExtendedFormItem
|
||||||
form={form}
|
form={form}
|
||||||
record={record}
|
record={record}
|
||||||
index={index}
|
index={index}
|
||||||
responsibilityCenters={responsibilityCenters}
|
responsibilityCenters={responsibilityCenters}
|
||||||
discount={discount}
|
discount={discount}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const data =
|
|
||||||
search === ""
|
|
||||||
? lineData
|
|
||||||
: lineData.filter(
|
|
||||||
(l) =>
|
|
||||||
(l.line_desc &&
|
|
||||||
l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
|
|
||||||
(l.oem_partno &&
|
|
||||||
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
|
|
||||||
(l.act_price &&
|
|
||||||
l.act_price.toString().startsWith(search.toString()))
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const data =
|
||||||
|
search === ""
|
||||||
|
? lineData
|
||||||
|
: lineData.filter(
|
||||||
|
(l) =>
|
||||||
|
(l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
|
||||||
|
(l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
|
||||||
|
(l.act_price && l.act_price.toString().startsWith(search.toString()))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,284 +1,216 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons";
|
import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
|
||||||
import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd";
|
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import CiecaSelect from "../../utils/Ciecaselect";
|
import CiecaSelect from "../../utils/Ciecaselect";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillFormItemsExtendedFormItem);
|
|
||||||
|
|
||||||
export function BillFormItemsExtendedFormItem({
|
export function BillFormItemsExtendedFormItem({
|
||||||
value,
|
value,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
form,
|
form,
|
||||||
record,
|
record,
|
||||||
index,
|
index,
|
||||||
disabled,
|
disabled,
|
||||||
responsibilityCenters,
|
responsibilityCenters,
|
||||||
discount,
|
discount
|
||||||
}) {
|
}) {
|
||||||
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
||||||
|
|
||||||
const {t} = useTranslation();
|
|
||||||
if (!value)
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const values = form.getFieldsValue("billlineskeys");
|
|
||||||
|
|
||||||
form.setFieldsValue({
|
|
||||||
...values,
|
|
||||||
billlineskeys: {
|
|
||||||
...(values.billlineskeys || {}),
|
|
||||||
[record.id]: {
|
|
||||||
joblineid: record.id,
|
|
||||||
line_desc: record.line_desc,
|
|
||||||
quantity: record.part_qty || 1,
|
|
||||||
actual_price: record.act_price,
|
|
||||||
cost_center: record.part_type
|
|
||||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
|
||||||
? record.part_type
|
|
||||||
: responsibilityCenters.defaults &&
|
|
||||||
(responsibilityCenters.defaults.costs[record.part_type] ||
|
|
||||||
null)
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusCircleFilled/>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (!value)
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Button
|
||||||
<Form.Item
|
onClick={() => {
|
||||||
label={t("billlines.fields.line_desc")}
|
const values = form.getFieldsValue("billlineskeys");
|
||||||
name={["billlineskeys", record.id, "line_desc"]}
|
|
||||||
>
|
|
||||||
<Input disabled={disabled}/>
|
|
||||||
</Form.Item>
|
|
||||||
<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"]}
|
|
||||||
>
|
|
||||||
<CurrencyInput
|
|
||||||
min={0}
|
|
||||||
disabled={disabled}
|
|
||||||
onBlur={(e) => {
|
|
||||||
const {billlineskeys} = form.getFieldsValue("billlineskeys");
|
|
||||||
form.setFieldsValue({
|
|
||||||
billlineskeys: {
|
|
||||||
...billlineskeys,
|
|
||||||
[record.id]: {
|
|
||||||
...billlineskeys[billlineskeys],
|
|
||||||
actual_cost: !!billlineskeys[billlineskeys].actual_cost
|
|
||||||
? billlineskeys[billlineskeys].actual_cost
|
|
||||||
: Math.round(
|
|
||||||
(parseFloat(e.target.value) * (1 - discount) +
|
|
||||||
Number.EPSILON) *
|
|
||||||
100
|
|
||||||
) / 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<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);
|
|
||||||
|
|
||||||
if (lineDiscount - discount === 0) return <div/>;
|
form.setFieldsValue({
|
||||||
return <WarningOutlined style={{color: "red"}}/>;
|
...values,
|
||||||
}}
|
billlineskeys: {
|
||||||
</Form.Item>
|
...(values.billlineskeys || {}),
|
||||||
<Form.Item
|
[record.id]: {
|
||||||
label={t("billlines.fields.cost_center")}
|
joblineid: record.id,
|
||||||
name={["billlineskeys", record.id, "cost_center"]}
|
line_desc: record.line_desc,
|
||||||
>
|
quantity: record.part_qty || 1,
|
||||||
<Select showSearch style={{minWidth: "3rem"}} disabled={disabled}>
|
actual_price: record.act_price,
|
||||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
cost_center: record.part_type
|
||||||
? CiecaSelect(true, false)
|
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||||
: responsibilityCenters.costs.map((item) => (
|
? record.part_type
|
||||||
<Select.Option key={item.name}>{item.name}</Select.Option>
|
: responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
|
||||||
))}
|
: null
|
||||||
</Select>
|
}
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
});
|
||||||
label={t("billlines.fields.location")}
|
}}
|
||||||
name={["billlineskeys", record.id, "location"]}
|
>
|
||||||
>
|
<PlusCircleFilled />
|
||||||
<Select disabled={disabled}>
|
</Button>
|
||||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
|
||||||
<Select.Option key={idx} value={loc}>
|
|
||||||
{loc}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("billlines.fields.deductedfromlbr")}
|
|
||||||
name={["billlineskeys", record.id, "deductedfromlbr"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch disabled={disabled}/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item shouldUpdate style={{display: "inline-block"}}>
|
|
||||||
{() => {
|
|
||||||
if (
|
|
||||||
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
|
|
||||||
.deductedfromlbr
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.mod_lbr_ty")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
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>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.labels.adjustmentrate")}
|
|
||||||
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
|
|
||||||
initialValue={bodyshop.default_adjustment_rate}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber precision={2} min={0.01}/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return <></>;
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t("billlines.fields.federal_tax_applicable")}
|
|
||||||
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch disabled={disabled}/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("billlines.fields.state_tax_applicable")}
|
|
||||||
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch disabled={disabled}/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("billlines.fields.local_tax_applicable")}
|
|
||||||
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch disabled={disabled}/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const values = form.getFieldsValue("billlineskeys");
|
|
||||||
|
|
||||||
form.setFieldsValue({
|
|
||||||
...values,
|
|
||||||
billlineskeys: {
|
|
||||||
...(values.billlineskeys || {}),
|
|
||||||
[record.id]: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MinusCircleFilled/>
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<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"]}>
|
||||||
|
<InputNumber precision={0} min={0} disabled={disabled} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("billlines.fields.actual_price")} name={["billlineskeys", record.id, "actual_price"]}>
|
||||||
|
<CurrencyInput
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
||||||
|
form.setFieldsValue({
|
||||||
|
billlineskeys: {
|
||||||
|
...billlineskeys,
|
||||||
|
[record.id]: {
|
||||||
|
...billlineskeys[billlineskeys],
|
||||||
|
actual_cost: !!billlineskeys[billlineskeys].actual_cost
|
||||||
|
? billlineskeys[billlineskeys].actual_cost
|
||||||
|
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<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);
|
||||||
|
|
||||||
|
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"]}>
|
||||||
|
<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>)}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<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}>
|
||||||
|
{loc}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("billlines.fields.deductedfromlbr")}
|
||||||
|
name={["billlineskeys", record.id, "deductedfromlbr"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch disabled={disabled} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
|
||||||
|
{() => {
|
||||||
|
if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={t("joblines.fields.mod_lbr_ty")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
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>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.labels.adjustmentrate")}
|
||||||
|
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
|
||||||
|
initialValue={bodyshop.default_adjustment_rate}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber precision={2} min={0.01} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return <></>;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("billlines.fields.federal_tax_applicable")}
|
||||||
|
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch disabled={disabled} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("billlines.fields.state_tax_applicable")}
|
||||||
|
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch disabled={disabled} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("billlines.fields.local_tax_applicable")}
|
||||||
|
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch disabled={disabled} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const values = form.getFieldsValue("billlineskeys");
|
||||||
|
|
||||||
|
form.setFieldsValue({
|
||||||
|
...values,
|
||||||
|
billlineskeys: {
|
||||||
|
...(values.billlineskeys || {}),
|
||||||
|
[record.id]: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusCircleFilled />
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import Icon, { UploadOutlined } from '@ant-design/icons';
|
import Icon, { UploadOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { useSplitTreatments } from '@splitsoftware/splitio-react';
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd';
|
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdOpenInNew } from 'react-icons/md';
|
import { MdOpenInNew } from "react-icons/md";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from 'reselect';
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries';
|
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
||||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import dayjs from '../../utils/day';
|
import dayjs from "../../utils/day";
|
||||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import AlertComponent from '../alert/alert.component';
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component';
|
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
||||||
import FormDatePicker from '../form-date-picker/form-date-picker.component';
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component';
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from '../form-items-formatted/currency-form-item.component';
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobSearchSelect from '../job-search-select/job-search-select.component';
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
import LayoutFormRow from '../layout-form-row/layout-form-row.component';
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component';
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import BillFormLines from './bill-form.lines.component';
|
import BillFormLines from "./bill-form.lines.component";
|
||||||
import { CalculateBillTotal } from './bill-form.totals.utility';
|
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
@@ -41,18 +41,18 @@ export function BillFormComponent({
|
|||||||
job,
|
job,
|
||||||
loadOutstandingReturns,
|
loadOutstandingReturns,
|
||||||
loadInventory,
|
loadInventory,
|
||||||
preferredMake,
|
preferredMake
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [discount, setDiscount] = useState(0);
|
const [discount, setDiscount] = useState(0);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Extended_Bill_Posting, ClosingPeriod },
|
treatments: { Extended_Bill_Posting, ClosingPeriod }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
names: ['Extended_Bill_Posting', 'ClosingPeriod'],
|
names: ["Extended_Bill_Posting", "ClosingPeriod"],
|
||||||
splitKey: bodyshop.imexshopid,
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVendorSelect = (props, opt) => {
|
const handleVendorSelect = (props, opt) => {
|
||||||
@@ -62,16 +62,16 @@ export function BillFormComponent({
|
|||||||
!billEdit &&
|
!billEdit &&
|
||||||
loadOutstandingReturns({
|
loadOutstandingReturns({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: form.getFieldValue('jobid'),
|
jobId: form.getFieldValue("jobid"),
|
||||||
vendorId: opt.value,
|
vendorId: opt.value
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
||||||
// Early gate
|
// Early gate
|
||||||
if (!checked) return;
|
if (!checked) return;
|
||||||
const values = form.getFieldsValue('billlines');
|
const values = form.getFieldsValue("billlines");
|
||||||
// Gate bill lines
|
// Gate bill lines
|
||||||
if (!values?.billlines?.length) return;
|
if (!values?.billlines?.length) return;
|
||||||
|
|
||||||
@@ -83,26 +83,26 @@ export function BillFormComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (job) form.validateFields(['is_credit_memo']);
|
if (job) form.validateFields(["is_credit_memo"]);
|
||||||
}, [job, form]);
|
}, [job, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vendorId = form.getFieldValue('vendorid');
|
const vendorId = form.getFieldValue("vendorid");
|
||||||
if (vendorId && vendorAutoCompleteOptions) {
|
if (vendorId && vendorAutoCompleteOptions) {
|
||||||
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
|
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
|
||||||
if (matchingVendors.length === 1) {
|
if (matchingVendors.length === 1) {
|
||||||
setDiscount(matchingVendors[0].discount);
|
setDiscount(matchingVendors[0].discount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const jobId = form.getFieldValue('jobid');
|
const jobId = form.getFieldValue("jobid");
|
||||||
if (jobId) {
|
if (jobId) {
|
||||||
loadLines({ variables: { id: jobId } });
|
loadLines({ variables: { id: jobId } });
|
||||||
if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) {
|
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
||||||
loadOutstandingReturns({
|
loadOutstandingReturns({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
vendorId: vendorId,
|
vendorId: vendorId
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,24 +118,24 @@ export function BillFormComponent({
|
|||||||
setDiscount,
|
setDiscount,
|
||||||
vendorAutoCompleteOptions,
|
vendorAutoCompleteOptions,
|
||||||
loadLines,
|
loadLines,
|
||||||
bodyshop.inhousevendorid,
|
bodyshop.inhousevendorid
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormFieldsChanged form={form} />
|
<FormFieldsChanged form={form} />
|
||||||
<Form.Item style={{ display: 'none' }} name="isinhouse" valuePropName="checked">
|
<Form.Item style={{ display: "none" }} name="isinhouse" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="jobid"
|
name="jobid"
|
||||||
label={t('bills.fields.ro_number')}
|
label={t("bills.fields.ro_number")}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<JobSearchSelect
|
<JobSearchSelect
|
||||||
@@ -143,20 +143,14 @@ export function BillFormComponent({
|
|||||||
convertedOnly
|
convertedOnly
|
||||||
notExported={false}
|
notExported={false}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (
|
if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
|
||||||
form.getFieldValue('jobid') !== null &&
|
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||||
form.getFieldValue('jobid') !== undefined
|
if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
|
||||||
) {
|
|
||||||
loadLines({ variables: { id: form.getFieldValue('jobid') } });
|
|
||||||
if (
|
|
||||||
form.getFieldValue('vendorid') !== null &&
|
|
||||||
form.getFieldValue('vendorid') !== undefined
|
|
||||||
) {
|
|
||||||
loadOutstandingReturns({
|
loadOutstandingReturns({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: form.getFieldValue('jobid'),
|
jobId: form.getFieldValue("jobid"),
|
||||||
vendorId: form.getFieldValue('vendorid'),
|
vendorId: form.getFieldValue("vendorid")
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,22 +158,22 @@ export function BillFormComponent({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('bills.fields.vendor')}
|
label={t("bills.fields.vendor")}
|
||||||
name="vendorid"
|
name="vendorid"
|
||||||
// style={{ display: billEdit ? "none" : null }}
|
// style={{ display: billEdit ? "none" : null }}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) {
|
if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) {
|
||||||
return Promise.reject(t('bills.validation.manualinhouse'));
|
return Promise.reject(t("bills.validation.manualinhouse"));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<VendorSearchSelect
|
<VendorSearchSelect
|
||||||
@@ -199,12 +193,8 @@ export function BillFormComponent({
|
|||||||
type="warning"
|
type="warning"
|
||||||
message={
|
message={
|
||||||
<Space>
|
<Space>
|
||||||
{t('bills.labels.iouexists')}
|
{t("bills.labels.iouexists")}
|
||||||
<Link
|
<Link target="_blank" rel="noopener noreferrer" to={`/manage/jobs/${iou.id}?tab=repairdata`}>
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
to={`/manage/jobs/${iou.id}?tab=repairdata`}
|
|
||||||
>
|
|
||||||
<Space>
|
<Space>
|
||||||
{iou.ro_number}
|
{iou.ro_number}
|
||||||
<Icon component={MdOpenInNew} />
|
<Icon component={MdOpenInNew} />
|
||||||
@@ -216,89 +206,85 @@ export function BillFormComponent({
|
|||||||
))}
|
))}
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('bills.fields.invoice_number')}
|
label={t("bills.fields.invoice_number")}
|
||||||
name="invoice_number"
|
name="invoice_number"
|
||||||
validateTrigger="onBlur"
|
validateTrigger="onBlur"
|
||||||
hasFeedback
|
hasFeedback
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
async validator(rule, value) {
|
async validator(rule, value) {
|
||||||
const vendorid = getFieldValue('vendorid');
|
const vendorid = getFieldValue("vendorid");
|
||||||
if (vendorid && value) {
|
if (vendorid && value) {
|
||||||
const response = await client.query({
|
const response = await client.query({
|
||||||
query: CHECK_BILL_INVOICE_NUMBER,
|
query: CHECK_BILL_INVOICE_NUMBER,
|
||||||
variables: {
|
variables: {
|
||||||
invoice_number: value,
|
invoice_number: value,
|
||||||
vendorid: vendorid,
|
vendorid: vendorid
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else if (
|
} else if (
|
||||||
response.data.bills_aggregate.nodes.length === 1 &&
|
response.data.bills_aggregate.nodes.length === 1 &&
|
||||||
response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id')
|
response.data.bills_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||||
) {
|
) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject(t('bills.validation.unique_invoice_number'));
|
return Promise.reject(t("bills.validation.unique_invoice_number"));
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input disabled={disabled || disableInvNumber} />
|
<Input disabled={disabled || disableInvNumber} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('bills.fields.date')}
|
label={t("bills.fields.date")}
|
||||||
name="date"
|
name="date"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) {
|
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
|
||||||
if (
|
if (
|
||||||
dayjs(value)
|
dayjs(value)
|
||||||
.startOf('day')
|
.startOf("day")
|
||||||
.isSameOrAfter(
|
.isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
|
||||||
dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
|
|
||||||
) &&
|
|
||||||
dayjs(value)
|
dayjs(value)
|
||||||
.startOf('day')
|
.startOf("day")
|
||||||
.isSameOrBefore(
|
.isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day"))
|
||||||
dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(t('bills.validation.closingperiod'));
|
return Promise.reject(t("bills.validation.closingperiod"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker disabled={disabled} />
|
<FormDatePicker disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('bills.fields.is_credit_memo')}
|
label={t("bills.fields.is_credit_memo")}
|
||||||
name="is_credit_memo"
|
name="is_credit_memo"
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) {
|
if (value === true && getFieldValue("jobid") && getFieldValue("vendorid")) {
|
||||||
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
||||||
// loadOutstandingReturns({
|
// loadOutstandingReturns({
|
||||||
// variables: {
|
// variables: {
|
||||||
@@ -316,31 +302,31 @@ export function BillFormComponent({
|
|||||||
job.status === bodyshop.md_ro_statuses.default_void) &&
|
job.status === bodyshop.md_ro_statuses.default_void) &&
|
||||||
(value === false || !value)
|
(value === false || !value)
|
||||||
) {
|
) {
|
||||||
return Promise.reject(t('bills.labels.onlycmforinvoiced'));
|
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('bills.fields.total')}
|
label={t("bills.fields.total")}
|
||||||
name="total"
|
name="total"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} disabled={disabled} />
|
<CurrencyInput min={0} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{!billEdit && (
|
{!billEdit && (
|
||||||
<Form.Item label={t('bills.fields.allpartslocation')} name="location">
|
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||||
<Select style={{ width: '10rem' }} disabled={disabled} allowClear>
|
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||||
<Select.Option key={idx} value={loc}>
|
<Select.Option key={idx} value={loc}>
|
||||||
{loc}
|
{loc}
|
||||||
@@ -353,40 +339,36 @@ export function BillFormComponent({
|
|||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex: (
|
imex: (
|
||||||
<Form.Item span={3} label={t('bills.fields.federal_tax_rate')} name="federal_tax_rate">
|
<Form.Item span={3} label={t("bills.fields.federal_tax_rate")} name="federal_tax_rate">
|
||||||
<CurrencyInput min={0} disabled={disabled} />
|
<CurrencyInput min={0} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
),
|
)
|
||||||
})}
|
})}
|
||||||
<Form.Item span={3} label={t('bills.fields.state_tax_rate')} name="state_tax_rate">
|
<Form.Item span={3} label={t("bills.fields.state_tax_rate")} name="state_tax_rate">
|
||||||
<CurrencyInput min={0} disabled={disabled} />
|
<CurrencyInput min={0} disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex: (
|
imex: (
|
||||||
<>
|
<>
|
||||||
<Form.Item span={3} label={t('bills.fields.local_tax_rate')} name="local_tax_rate">
|
<Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||||
<Form.Item
|
<Form.Item span={2} label={t("bills.labels.federal_tax_exempt")} name="federal_tax_exempt">
|
||||||
span={2}
|
|
||||||
label={t('bills.labels.federal_tax_exempt')}
|
|
||||||
name="federal_tax_exempt"
|
|
||||||
>
|
|
||||||
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
),
|
)
|
||||||
})}
|
})}
|
||||||
<Form.Item shouldUpdate span={13}>
|
<Form.Item shouldUpdate span={13}>
|
||||||
{() => {
|
{() => {
|
||||||
const values = form.getFieldsValue([
|
const values = form.getFieldsValue([
|
||||||
'billlines',
|
"billlines",
|
||||||
'total',
|
"total",
|
||||||
'federal_tax_rate',
|
"federal_tax_rate",
|
||||||
'state_tax_rate',
|
"state_tax_rate",
|
||||||
'local_tax_rate',
|
"local_tax_rate"
|
||||||
]);
|
]);
|
||||||
let totals;
|
let totals;
|
||||||
if (!!values.total && !!values.billlines && values.billlines.length > 0)
|
if (!!values.total && !!values.billlines && values.billlines.length > 0)
|
||||||
@@ -394,56 +376,48 @@ export function BillFormComponent({
|
|||||||
if (!!totals)
|
if (!!totals)
|
||||||
return (
|
return (
|
||||||
<div align="right">
|
<div align="right">
|
||||||
<Space size='large' wrap>
|
<Space size="large" wrap>
|
||||||
<Statistic
|
<Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
|
||||||
title={t('bills.labels.subtotal')}
|
|
||||||
value={totals.subtotal.toFormat()}
|
|
||||||
precision={2}
|
|
||||||
/>
|
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex: (
|
imex: (
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('bills.labels.federal_tax')}
|
title={t("bills.labels.federal_tax")}
|
||||||
value={totals.federalTax.toFormat()}
|
value={totals.federalTax.toFormat()}
|
||||||
precision={2}
|
precision={2}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
})}
|
})}
|
||||||
<Statistic
|
<Statistic title={t("bills.labels.state_tax")} value={totals.stateTax.toFormat()} precision={2} />
|
||||||
title={t('bills.labels.state_tax')}
|
|
||||||
value={totals.stateTax.toFormat()}
|
|
||||||
precision={2}
|
|
||||||
/>
|
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex: (
|
imex: (
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('bills.labels.local_tax')}
|
title={t("bills.labels.local_tax")}
|
||||||
value={totals.localTax.toFormat()}
|
value={totals.localTax.toFormat()}
|
||||||
precision={2}
|
precision={2}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
})}
|
})}
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('bills.labels.entered_total')}
|
title={t("bills.labels.entered_total")}
|
||||||
value={totals.enteredTotal.toFormat()}
|
value={totals.enteredTotal.toFormat()}
|
||||||
precision={2}
|
precision={2}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('bills.labels.bill_total')}
|
title={t("bills.labels.bill_total")}
|
||||||
value={totals.invoiceTotal.toFormat()}
|
value={totals.invoiceTotal.toFormat()}
|
||||||
precision={2}
|
precision={2}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('bills.labels.discrepancy')}
|
title={t("bills.labels.discrepancy")}
|
||||||
valueStyle={{
|
valueStyle={{
|
||||||
color: totals.discrepancy.getAmount() === 0 ? 'green' : 'red',
|
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
|
||||||
}}
|
}}
|
||||||
value={totals.discrepancy.toFormat()}
|
value={totals.discrepancy.toFormat()}
|
||||||
precision={2}
|
precision={2}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
{form.getFieldValue('is_credit_memo') ? (
|
{form.getFieldValue("is_credit_memo") ? (
|
||||||
<AlertComponent type="warning" message={t('bills.labels.enteringcreditmemo')} />
|
<AlertComponent type="warning" message={t("bills.labels.enteringcreditmemo")} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -451,9 +425,9 @@ export function BillFormComponent({
|
|||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t('bills.labels.bill_lines')}</Divider>
|
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
||||||
|
|
||||||
{Extended_Bill_Posting.treatment === 'on' ? (
|
{Extended_Bill_Posting.treatment === "on" ? (
|
||||||
<BillFormLinesExtended
|
<BillFormLinesExtended
|
||||||
lineData={lineData}
|
lineData={lineData}
|
||||||
discount={discount}
|
discount={discount}
|
||||||
@@ -471,13 +445,13 @@ export function BillFormComponent({
|
|||||||
billEdit={billEdit}
|
billEdit={billEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Divider orientation="left" style={{ display: billEdit ? 'none' : null }}>
|
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
|
||||||
{t('documents.labels.upload')}
|
{t("documents.labels.upload")}
|
||||||
</Divider>
|
</Divider>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="upload"
|
name="upload"
|
||||||
label="Upload"
|
label="Upload"
|
||||||
style={{ display: billEdit ? 'none' : null }}
|
style={{ display: billEdit ? "none" : null }}
|
||||||
valuePropName="fileList"
|
valuePropName="fileList"
|
||||||
getValueFromEvent={(e) => {
|
getValueFromEvent={(e) => {
|
||||||
if (Array.isArray(e)) {
|
if (Array.isArray(e)) {
|
||||||
|
|||||||
@@ -1,83 +1,67 @@
|
|||||||
import {useLazyQuery, useQuery} from "@apollo/client";
|
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries";
|
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||||
import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries";
|
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
||||||
import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries";
|
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||||
import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries";
|
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||||
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||||
import BillFormComponent from "./bill-form.component";
|
import BillFormComponent from "./bill-form.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BillFormContainer({
|
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
||||||
bodyshop,
|
const {
|
||||||
form,
|
treatments: { Simple_Inventory }
|
||||||
billEdit,
|
} = useSplitTreatments({
|
||||||
disabled,
|
attributes: {},
|
||||||
disableInvNumber,
|
names: ["Simple_Inventory"],
|
||||||
}) {
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
const {treatments: {Simple_Inventory}} = useSplitTreatments({
|
});
|
||||||
attributes: {},
|
|
||||||
names: ["Simple_Inventory"],
|
|
||||||
splitKey: bodyshop && bodyshop.imexshopid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {data: VendorAutoCompleteData} = useQuery(
|
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
|
||||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
fetchPolicy: "network-only",
|
||||||
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
|
nextFetchPolicy: "network-only"
|
||||||
);
|
});
|
||||||
|
|
||||||
const [loadLines, {data: lineData}] = useLazyQuery(
|
const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
|
||||||
GET_JOB_LINES_TO_ENTER_BILL
|
|
||||||
);
|
|
||||||
|
|
||||||
const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] =
|
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
|
||||||
useLazyQuery(QUERY_UNRECEIVED_LINES);
|
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
||||||
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
|
|
||||||
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BillFormComponent
|
<BillFormComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
form={form}
|
form={form}
|
||||||
billEdit={billEdit}
|
billEdit={billEdit}
|
||||||
vendorAutoCompleteOptions={
|
vendorAutoCompleteOptions={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
|
||||||
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
loadLines={loadLines}
|
||||||
}
|
lineData={lineData ? lineData.joblines : []}
|
||||||
loadLines={loadLines}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
lineData={lineData ? lineData.joblines : []}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
job={lineData ? lineData.jobs_by_pk : null}
|
disableInvNumber={disableInvNumber}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
disableInvNumber={disableInvNumber}
|
loadInventory={loadInventory}
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||||
loadInventory={loadInventory}
|
/>
|
||||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
{!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
|
||||||
/>
|
{Simple_Inventory.treatment === "on" && (
|
||||||
{!billEdit && (
|
<BillInventoryTable
|
||||||
<BillCmdReturnsTableComponent
|
form={form}
|
||||||
form={form}
|
inventoryLoading={inventoryLoading}
|
||||||
returnLoading={returnLoading}
|
inventoryData={billEdit ? [] : inventoryData}
|
||||||
returnData={returnData}
|
billEdit={billEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Simple_Inventory.treatment === "on" && (
|
</>
|
||||||
<BillInventoryTable
|
);
|
||||||
form={form}
|
|
||||||
inventoryLoading={inventoryLoading}
|
|
||||||
inventoryData={billEdit ? [] : inventoryData}
|
|
||||||
billEdit={billEdit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(BillFormContainer);
|
export default connect(mapStateToProps, null)(BillFormContainer);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +1,42 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
|
|
||||||
export const CalculateBillTotal = (invoice) => {
|
export const CalculateBillTotal = (invoice) => {
|
||||||
const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} =
|
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
|
||||||
invoice;
|
|
||||||
|
|
||||||
//TODO Determine why this recalculates so many times.
|
//TODO Determine why this recalculates so many times.
|
||||||
let subtotal = Dinero({amount: 0});
|
let subtotal = Dinero({ amount: 0 });
|
||||||
let federalTax = Dinero({amount: 0});
|
let federalTax = Dinero({ amount: 0 });
|
||||||
let stateTax = Dinero({amount: 0});
|
let stateTax = Dinero({ amount: 0 });
|
||||||
let localTax = Dinero({amount: 0});
|
let localTax = Dinero({ amount: 0 });
|
||||||
|
|
||||||
if (!!!billlines) return null;
|
if (!!!billlines) return null;
|
||||||
|
|
||||||
billlines.forEach((i) => {
|
billlines.forEach((i) => {
|
||||||
if (!!i) {
|
if (!!i) {
|
||||||
const itemTotal = Dinero({
|
const itemTotal = Dinero({
|
||||||
amount: Math.round((i.actual_cost || 0) * 100),
|
amount: Math.round((i.actual_cost || 0) * 100)
|
||||||
}).multiply(i.quantity || 1);
|
}).multiply(i.quantity || 1);
|
||||||
|
|
||||||
subtotal = subtotal.add(itemTotal);
|
subtotal = subtotal.add(itemTotal);
|
||||||
if (i.applicable_taxes?.federal) {
|
if (i.applicable_taxes?.federal) {
|
||||||
federalTax = federalTax.add(
|
federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0));
|
||||||
itemTotal.percentage(federal_tax_rate || 0)
|
}
|
||||||
);
|
if (i.applicable_taxes?.state) 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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)});
|
const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) });
|
||||||
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
|
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
|
||||||
const discrepancy = enteredTotal.subtract(invoiceTotal);
|
const discrepancy = enteredTotal.subtract(invoiceTotal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal,
|
subtotal,
|
||||||
federalTax,
|
federalTax,
|
||||||
stateTax,
|
stateTax,
|
||||||
localTax,
|
localTax,
|
||||||
enteredTotal,
|
enteredTotal,
|
||||||
invoiceTotal,
|
invoiceTotal,
|
||||||
discrepancy,
|
discrepancy
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,173 +1,153 @@
|
|||||||
import {Checkbox, Form, Skeleton, Typography} from "antd";
|
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||||
import React, {useEffect} from "react";
|
import React, { useEffect } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
import "./bill-inventory-table.styles.scss";
|
import "./bill-inventory-table.styles.scss";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
|
||||||
|
|
||||||
export function BillInventoryTable({
|
export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
|
||||||
billEnterModal,
|
const { t } = useTranslation();
|
||||||
bodyshop,
|
|
||||||
form,
|
|
||||||
billEdit,
|
|
||||||
inventoryLoading,
|
|
||||||
inventoryData,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inventoryData && inventoryData.inventory) {
|
if (inventoryData && inventoryData.inventory) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
inventory: billEnterModal.context.consumeinventoryid
|
inventory: billEnterModal.context.consumeinventoryid
|
||||||
? inventoryData.inventory.map((i) => {
|
? inventoryData.inventory.map((i) => {
|
||||||
if (i.id === billEnterModal.context.consumeinventoryid)
|
if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
|
||||||
i.consumefrominventory = true;
|
return i;
|
||||||
return i;
|
})
|
||||||
})
|
: inventoryData.inventory
|
||||||
: inventoryData.inventory,
|
});
|
||||||
});
|
}
|
||||||
|
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid} noStyle>
|
||||||
|
{() => {
|
||||||
|
const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||||
|
|
||||||
|
if (!is_inhouse || billEdit) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
|
||||||
|
|
||||||
return (
|
if (inventoryLoading) return <Skeleton />;
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const is_inhouse =
|
|
||||||
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
|
||||||
|
|
||||||
if (!is_inhouse || billEdit) {
|
return (
|
||||||
return null;
|
<Form.List name="inventory">
|
||||||
}
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={4}>{t("inventory.labels.inventory")}</Typography.Title>
|
||||||
|
<table className="bill-inventory-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("billlines.fields.line_desc")}</th>
|
||||||
|
<th>{t("vendors.fields.name")}</th>
|
||||||
|
<th>{t("billlines.fields.quantity")}</th>
|
||||||
|
<th>{t("billlines.fields.actual_price")}</th>
|
||||||
|
<th>{t("billlines.fields.actual_cost")}</th>
|
||||||
|
<th>{t("inventory.fields.comment")}</th>
|
||||||
|
<th>{t("inventory.actions.consumefrominventory")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<tr key={field.key}>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
// label={t("joblines.fields.line_desc")}
|
||||||
|
key={`${index}line_desc`}
|
||||||
|
name={[field.name, "line_desc"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
if (inventoryLoading) return <Skeleton/>;
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}part_type`}
|
||||||
|
name={[field.name, "billline", "bill", "vendor", "name"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}quantity`}
|
||||||
|
name={[field.name, "quantity"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}act_price`}
|
||||||
|
name={[field.name, "actual_price"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}cost`}
|
||||||
|
name={[field.name, "actual_cost"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}comment`}
|
||||||
|
name={[field.name, "comment"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
return (
|
<td>
|
||||||
<Form.List name="inventory">
|
<Form.Item
|
||||||
{(fields, {add, remove, move}) => {
|
span={2}
|
||||||
return (
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
<>
|
key={`${index}consumefrominventory`}
|
||||||
<Typography.Title level={4}>
|
name={[field.name, "consumefrominventory"]}
|
||||||
{t("inventory.labels.inventory")}
|
valuePropName="checked"
|
||||||
</Typography.Title>
|
>
|
||||||
<table className="bill-inventory-table">
|
<Checkbox />
|
||||||
<thead>
|
</Form.Item>
|
||||||
<tr>
|
</td>
|
||||||
<th>{t("billlines.fields.line_desc")}</th>
|
</tr>
|
||||||
<th>{t("vendors.fields.name")}</th>
|
))}
|
||||||
<th>{t("billlines.fields.quantity")}</th>
|
</tbody>
|
||||||
<th>{t("billlines.fields.actual_price")}</th>
|
</table>
|
||||||
<th>{t("billlines.fields.actual_cost")}</th>
|
</>
|
||||||
<th>{t("inventory.fields.comment")}</th>
|
);
|
||||||
<th>{t("inventory.actions.consumefrominventory")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<tr key={field.key}>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
// label={t("joblines.fields.line_desc")}
|
|
||||||
key={`${index}line_desc`}
|
|
||||||
name={[field.name, "line_desc"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}part_type`}
|
|
||||||
name={[
|
|
||||||
field.name,
|
|
||||||
"billline",
|
|
||||||
"bill",
|
|
||||||
"vendor",
|
|
||||||
"name",
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}quantity`}
|
|
||||||
name={[field.name, "quantity"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}act_price`}
|
|
||||||
name={[field.name, "actual_price"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}cost`}
|
|
||||||
name={[field.name, "actual_cost"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent type="currency"/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}comment`}
|
|
||||||
name={[field.name, "comment"]}
|
|
||||||
>
|
|
||||||
<ReadOnlyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
|
||||||
key={`${index}consumefrominventory`}
|
|
||||||
name={[field.name, "consumefrominventory"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Checkbox/>
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.List>
|
||||||
);
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,73 @@
|
|||||||
import {Select} from "antd";
|
import { Select } from "antd";
|
||||||
import React, {forwardRef} from "react";
|
import React, { forwardRef } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InstanceRenderMgr from '../../utils/instanceRenderMgr';
|
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
//To be used as a form element only.
|
//To be used as a form element only.
|
||||||
const {Option} = Select;
|
const { Option } = Select;
|
||||||
const BillLineSearchSelect = (
|
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||||
{options, disabled, allowRemoved, ...restProps},
|
const { t } = useTranslation();
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
optionLabelProp={"name"}
|
optionLabelProp={"name"}
|
||||||
// optionFilterProp="line_desc"
|
// optionFilterProp="line_desc"
|
||||||
filterOption={(inputValue, option) => {
|
filterOption={(inputValue, option) => {
|
||||||
return (
|
return (
|
||||||
(option.line_desc &&
|
(option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||||
option.line_desc
|
(option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||||
.toLowerCase()
|
(option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||||
.includes(inputValue.toLowerCase())) ||
|
(option.act_price && option.act_price.toString().startsWith(inputValue.toString()))
|
||||||
(option.oem_partno &&
|
);
|
||||||
option.oem_partno
|
}}
|
||||||
.toLowerCase()
|
notFoundContent={"Removed."}
|
||||||
.includes(inputValue.toLowerCase())) ||
|
{...restProps}
|
||||||
(option.alt_partno &&
|
>
|
||||||
option.alt_partno
|
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||||
.toLowerCase()
|
{t("billlines.labels.other")}
|
||||||
.includes(inputValue.toLowerCase())) ||
|
</Select.Option>
|
||||||
(option.act_price &&
|
{options
|
||||||
option.act_price.toString().startsWith(inputValue.toString()))
|
? options.map((item) => (
|
||||||
);
|
<Option
|
||||||
}}
|
disabled={allowRemoved ? false : item.removed}
|
||||||
notFoundContent={"Removed."}
|
key={item.id}
|
||||||
{...restProps}
|
value={item.id}
|
||||||
>
|
cost={item.act_price ? item.act_price : 0}
|
||||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
part_type={item.part_type}
|
||||||
{t("billlines.labels.other")}
|
line_desc={item.line_desc}
|
||||||
</Select.Option>
|
part_qty={item.part_qty}
|
||||||
{options
|
oem_partno={item.oem_partno}
|
||||||
? options.map((item) => (
|
alt_partno={item.alt_partno}
|
||||||
<Option
|
act_price={item.act_price}
|
||||||
disabled={allowRemoved ? false : item.removed}
|
style={{
|
||||||
key={item.id}
|
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||||
value={item.id}
|
}}
|
||||||
cost={item.act_price ? item.act_price : 0}
|
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
part_type={item.part_type}
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
line_desc={item.line_desc}
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||||
part_qty={item.part_qty}
|
>
|
||||||
oem_partno={item.oem_partno}
|
|
||||||
alt_partno={item.alt_partno}
|
|
||||||
act_price={item.act_price}
|
|
||||||
style={{
|
|
||||||
...(item.removed ? {textDecoration: "line-through"} : {}),
|
|
||||||
}}
|
|
||||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||||
</span>
|
</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>
|
||||||
{
|
)
|
||||||
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.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
<span style={{float: "right", paddingleft: "1rem"}}>
|
|
||||||
{item.act_price
|
|
||||||
? `$${item.act_price && item.act_price.toFixed(2)}`
|
|
||||||
: ``}
|
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default forwardRef(BillLineSearchSelect);
|
export default forwardRef(BillLineSearchSelect);
|
||||||
|
|||||||
@@ -1,97 +1,89 @@
|
|||||||
import {gql, useMutation} from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import {Button, notification} from "antd";
|
import { Button, notification } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel,
|
authLevel: selectAuthLevel,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillMarkExportedButton);
|
|
||||||
|
|
||||||
export function BillMarkExportedButton({
|
export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
|
||||||
currentUser,
|
const { t } = useTranslation();
|
||||||
bodyshop,
|
const [loading, setLoading] = useState(false);
|
||||||
authLevel,
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
bill,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
|
||||||
|
|
||||||
const [updateBill] = useMutation(gql`
|
const [updateBill] = useMutation(gql`
|
||||||
mutation UPDATE_BILL($billId: uuid!) {
|
mutation UPDATE_BILL($billId: uuid!) {
|
||||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
|
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
exported
|
exported
|
||||||
exported_at
|
exported_at
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`);
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateBill({
|
const result = await updateBill({
|
||||||
variables: {billId: bill.id},
|
variables: { billId: bill.id }
|
||||||
});
|
|
||||||
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
billid: bill.id,
|
|
||||||
successful: true,
|
|
||||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.errors) {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("bills.successes.markexported"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("bills.errors.saving", {
|
|
||||||
error: JSON.stringify(result.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
//Get the owner details, populate it all back into the job.
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAccess = HasRbacAccess({
|
|
||||||
bodyshop,
|
|
||||||
authLevel,
|
|
||||||
action: "bills:reexport",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasAccess)
|
await insertExportLog({
|
||||||
return (
|
variables: {
|
||||||
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
logs: [
|
||||||
{t("bills.labels.markexported")}
|
{
|
||||||
</Button>
|
bodyshopid: bodyshop.id,
|
||||||
);
|
billid: bill.id,
|
||||||
|
successful: true,
|
||||||
|
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||||
|
useremail: currentUser.email
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return <></>;
|
if (!result.errors) {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("bills.successes.markexported")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
//Get the owner details, populate it all back into the job.
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAccess = HasRbacAccess({
|
||||||
|
bodyshop,
|
||||||
|
authLevel,
|
||||||
|
action: "bills:reexport"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasAccess)
|
||||||
|
return (
|
||||||
|
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||||
|
{t("bills.labels.markexported")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
import {Button, Space} from "antd";
|
import { Button, Space } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import {TemplateList} from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
export default function BillPrintButton({billid}) {
|
export default function BillPrintButton({ billid }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const Templates = TemplateList("job_special");
|
const Templates = TemplateList("job_special");
|
||||||
|
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await GenerateDocument(
|
await GenerateDocument(
|
||||||
{
|
{
|
||||||
name: Templates.parts_invoice_label_single.key,
|
name: Templates.parts_invoice_label_single.key,
|
||||||
variables: {
|
variables: {
|
||||||
id: billid,
|
id: billid
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"p"
|
"p"
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Warning: Error generating a document.");
|
console.warn("Warning: Error generating a document.");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button loading={loading} onClick={submitHandler}>
|
<Button loading={loading} onClick={submitHandler}>
|
||||||
{t("bills.labels.printlabels")}
|
{t("bills.labels.printlabels")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,72 @@
|
|||||||
import {gql, useMutation} from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import {Button, notification} from "antd";
|
import { Button, notification } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel,
|
authLevel: selectAuthLevel
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillMarkForReexportButton);
|
|
||||||
|
|
||||||
export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
|
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [updateBill] = useMutation(gql`
|
const [updateBill] = useMutation(gql`
|
||||||
mutation UPDATE_BILL($billId: uuid!) {
|
mutation UPDATE_BILL($billId: uuid!) {
|
||||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
|
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
exported
|
exported
|
||||||
exported_at
|
exported_at
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`);
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateBill({
|
const result = await updateBill({
|
||||||
variables: {billId: bill.id},
|
variables: { billId: bill.id }
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.errors) {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("bills.successes.reexport"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("bills.errors.saving", {
|
|
||||||
error: JSON.stringify(result.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
//Get the owner details, populate it all back into the job.
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAccess = HasRbacAccess({
|
|
||||||
bodyshop,
|
|
||||||
authLevel,
|
|
||||||
action: "bills:reexport",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasAccess)
|
if (!result.errors) {
|
||||||
return (
|
notification["success"]({
|
||||||
<Button
|
message: t("bills.successes.reexport")
|
||||||
loading={loading}
|
});
|
||||||
disabled={!bill.exported}
|
} else {
|
||||||
onClick={handleUpdate}
|
notification["error"]({
|
||||||
>
|
message: t("bills.errors.saving", {
|
||||||
{t("bills.labels.markforreexport")}
|
error: JSON.stringify(result.errors)
|
||||||
</Button>
|
})
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
//Get the owner details, populate it all back into the job.
|
||||||
|
};
|
||||||
|
|
||||||
return <></>;
|
const hasAccess = HasRbacAccess({
|
||||||
|
bodyshop,
|
||||||
|
authLevel,
|
||||||
|
action: "bills:reexport"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasAccess)
|
||||||
|
return (
|
||||||
|
<Button loading={loading} disabled={!bill.exported} onClick={handleUpdate}>
|
||||||
|
{t("bills.labels.markforreexport")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,151 +1,136 @@
|
|||||||
import {FileAddFilled} from "@ant-design/icons";
|
import { FileAddFilled } from "@ant-design/icons";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {Button, notification, Tooltip} from "antd";
|
import { Button, notification, Tooltip } from "antd";
|
||||||
import {t} from "i18next";
|
import { t } from "i18next";
|
||||||
import dayjs from "./../../utils/day";
|
import dayjs from "./../../utils/day";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries";
|
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
|
||||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import {useLocation} from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BilllineAddInventory);
|
|
||||||
|
|
||||||
export function BilllineAddInventory({
|
export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
|
||||||
currentUser,
|
const [loading, setLoading] = useState(false);
|
||||||
bodyshop,
|
const { billid } = queryString.parse(useLocation().search);
|
||||||
billline,
|
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||||
disabled,
|
|
||||||
jobid,
|
|
||||||
}) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const {billid} = queryString.parse(useLocation().search);
|
|
||||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
|
||||||
|
|
||||||
const addToInventory = async () => {
|
const addToInventory = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
//Check to make sure there are no existing items already in the inventory.
|
//Check to make sure there are no existing items already in the inventory.
|
||||||
|
|
||||||
const cm = {
|
const cm = {
|
||||||
vendorid: bodyshop.inhousevendorid,
|
vendorid: bodyshop.inhousevendorid,
|
||||||
invoice_number: "ih",
|
invoice_number: "ih",
|
||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
isinhouse: true,
|
isinhouse: true,
|
||||||
is_credit_memo: true,
|
is_credit_memo: true,
|
||||||
date: dayjs().format("YYYY-MM-DD"),
|
date: dayjs().format("YYYY-MM-DD"),
|
||||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||||
total: 0,
|
total: 0,
|
||||||
billlines: [
|
billlines: [
|
||||||
{
|
{
|
||||||
actual_price: billline.actual_price,
|
actual_price: billline.actual_price,
|
||||||
actual_cost: billline.actual_cost,
|
actual_cost: billline.actual_cost,
|
||||||
quantity: billline.quantity,
|
quantity: billline.quantity,
|
||||||
line_desc: billline.line_desc,
|
line_desc: billline.line_desc,
|
||||||
cost_center: billline.cost_center,
|
cost_center: billline.cost_center,
|
||||||
deductedfromlbr: billline.deductedfromlbr,
|
deductedfromlbr: billline.deductedfromlbr,
|
||||||
applicable_taxes: {
|
applicable_taxes: {
|
||||||
local: billline.applicable_taxes.local,
|
local: billline.applicable_taxes.local,
|
||||||
state: billline.applicable_taxes.state,
|
state: billline.applicable_taxes.state,
|
||||||
federal: billline.applicable_taxes.federal,
|
federal: billline.applicable_taxes.federal
|
||||||
},
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
|
||||||
|
|
||||||
const insertResult = await insertInventoryLine({
|
|
||||||
variables: {
|
|
||||||
joblineId:
|
|
||||||
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
|
|
||||||
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
|
||||||
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
|
||||||
inv: {
|
|
||||||
shopid: bodyshop.id,
|
|
||||||
billlineid: billline.id,
|
|
||||||
actual_price: billline.actual_price,
|
|
||||||
actual_cost: billline.actual_cost,
|
|
||||||
quantity: billline.quantity,
|
|
||||||
line_desc: billline.line_desc,
|
|
||||||
},
|
|
||||||
cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
|
|
||||||
pol: {
|
|
||||||
returnfrombill: billid,
|
|
||||||
vendorid: bodyshop.inhousevendorid,
|
|
||||||
deliver_by: dayjs().format("YYYY-MM-DD"),
|
|
||||||
parts_order_lines: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
line_desc: billline.line_desc,
|
|
||||||
|
|
||||||
act_price: billline.actual_price,
|
|
||||||
cost: billline.actual_cost,
|
|
||||||
quantity: billline.quantity,
|
|
||||||
job_line_id:
|
|
||||||
billline.joblineid === "noline" ? null : billline.joblineid,
|
|
||||||
part_type: billline.jobline && billline.jobline.part_type,
|
|
||||||
cm_received: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
order_date: "2022-06-01",
|
|
||||||
orderedby: currentUser.email,
|
|
||||||
jobid: jobid,
|
|
||||||
user_email: currentUser.email,
|
|
||||||
return: true,
|
|
||||||
status: "Ordered",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
refetchQueries: ["QUERY_BILL_BY_PK"],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!insertResult.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
message: t("inventory.successes.inserted"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("inventory.errors.inserting", {
|
|
||||||
error: JSON.stringify(insertResult.errors),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||||
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
|
||||||
<Button
|
const insertResult = await insertInventoryLine({
|
||||||
loading={loading}
|
variables: {
|
||||||
disabled={
|
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.
|
||||||
disabled || billline?.inventories?.length >= billline.quantity
|
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||||
}
|
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||||
onClick={addToInventory}
|
inv: {
|
||||||
>
|
shopid: bodyshop.id,
|
||||||
<FileAddFilled/>
|
billlineid: billline.id,
|
||||||
{billline?.inventories?.length > 0 && (
|
actual_price: billline.actual_price,
|
||||||
<div>({billline?.inventories?.length} in inv)</div>
|
actual_cost: billline.actual_cost,
|
||||||
)}
|
quantity: billline.quantity,
|
||||||
</Button>
|
line_desc: billline.line_desc
|
||||||
</Tooltip>
|
},
|
||||||
);
|
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
|
||||||
|
pol: {
|
||||||
|
returnfrombill: billid,
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
deliver_by: dayjs().format("YYYY-MM-DD"),
|
||||||
|
parts_order_lines: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
line_desc: billline.line_desc,
|
||||||
|
|
||||||
|
act_price: billline.actual_price,
|
||||||
|
cost: billline.actual_cost,
|
||||||
|
quantity: billline.quantity,
|
||||||
|
job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
|
||||||
|
part_type: billline.jobline && billline.jobline.part_type,
|
||||||
|
cm_received: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
order_date: "2022-06-01",
|
||||||
|
orderedby: currentUser.email,
|
||||||
|
jobid: jobid,
|
||||||
|
user_email: currentUser.email,
|
||||||
|
return: true,
|
||||||
|
status: "Ordered"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchQueries: ["QUERY_BILL_BY_PK"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!insertResult.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
message: t("inventory.successes.inserted")
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("inventory.errors.inserting", {
|
||||||
|
error: JSON.stringify(insertResult.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled || billline?.inventories?.length >= billline.quantity}
|
||||||
|
onClick={addToInventory}
|
||||||
|
>
|
||||||
|
<FileAddFilled />
|
||||||
|
{billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,236 +1,209 @@
|
|||||||
import {EditFilled, SyncOutlined} from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import {Button, Card, Checkbox, Input, Space, Table} from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectJobReadOnly} from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {DateFormatter} from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import {TemplateList} from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||||
setBillEnterContext: (context) =>
|
setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
|
||||||
dispatch(setModalContext({context: context, modal: "billEnter"})),
|
|
||||||
setReconciliationContext: (context) =>
|
|
||||||
dispatch(setModalContext({context: context, modal: "reconciliation"})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BillsListTableComponent({
|
export function BillsListTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobRO,
|
jobRO,
|
||||||
job,
|
job,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handleOnRowClick,
|
handleOnRowClick,
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
setBillEnterContext,
|
setBillEnterContext,
|
||||||
setReconciliationContext,
|
setReconciliationContext
|
||||||
}) {
|
}) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {}
|
||||||
});
|
});
|
||||||
// const search = queryString.parse(useLocation().search);
|
// const search = queryString.parse(useLocation().search);
|
||||||
// const selectedBill = search.billid;
|
// const selectedBill = search.billid;
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const Templates = TemplateList("bill");
|
const Templates = TemplateList("bill");
|
||||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||||
const { refetch } = billsQuery;
|
const { refetch } = billsQuery;
|
||||||
const recordActions = (record, showView = false) => (
|
const recordActions = (record, showView = false) => (
|
||||||
|
<Space wrap>
|
||||||
|
{showView && (
|
||||||
|
<Button onClick={() => handleOnRowClick(record)}>
|
||||||
|
<EditFilled />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{record.isinhouse && (
|
||||||
|
<PrintWrapperComponent
|
||||||
|
templateObject={{
|
||||||
|
name: Templates.inhouse_invoice.key,
|
||||||
|
variables: { id: record.id }
|
||||||
|
}}
|
||||||
|
messageObject={{ subject: Templates.inhouse_invoice.subject }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("bills.fields.vendorname"),
|
||||||
|
dataIndex: "vendorname",
|
||||||
|
key: "vendorname",
|
||||||
|
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
render: (text, record) => recordActions(record, true)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredBills = bills
|
||||||
|
? searchText === ""
|
||||||
|
? bills
|
||||||
|
: bills.filter(
|
||||||
|
(b) =>
|
||||||
|
(b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
(b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("bills.labels.bills")}
|
||||||
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{showView && (
|
<Button onClick={() => refetch()}>
|
||||||
<Button onClick={() => handleOnRowClick(record)}>
|
<SyncOutlined />
|
||||||
<EditFilled />
|
</Button>
|
||||||
</Button>
|
{job && job.converted ? (
|
||||||
)}
|
<>
|
||||||
<BillDeleteButton bill={record} jobid={job.id} />
|
<Button
|
||||||
<BillDetailEditReturnComponent
|
onClick={() => {
|
||||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
setBillEnterContext({
|
||||||
disabled={
|
actions: { refetch: billsQuery.refetch },
|
||||||
record.is_credit_memo ||
|
context: {
|
||||||
record.vendorid === bodyshop.inhousevendorid ||
|
job
|
||||||
jobRO
|
}
|
||||||
}
|
});
|
||||||
/>
|
|
||||||
|
|
||||||
{record.isinhouse && (
|
|
||||||
<PrintWrapperComponent
|
|
||||||
templateObject={{
|
|
||||||
name: Templates.inhouse_invoice.key,
|
|
||||||
variables: {id: record.id},
|
|
||||||
}}
|
|
||||||
messageObject={{subject: Templates.inhouse_invoice.subject}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t("bills.fields.vendorname"),
|
|
||||||
dataIndex: "vendorname",
|
|
||||||
key: "vendorname",
|
|
||||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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}/>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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}/>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("general.labels.actions"),
|
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
render: (text, record) => recordActions(record, true),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredBills = bills
|
|
||||||
? searchText === ""
|
|
||||||
? bills
|
|
||||||
: bills.filter(
|
|
||||||
(b) =>
|
|
||||||
(b.invoice_number || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchText.toLowerCase()) ||
|
|
||||||
(b.vendor.name || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchText.toLowerCase()) ||
|
|
||||||
(b.total || "")
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchText.toLowerCase())
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={t("bills.labels.bills")}
|
|
||||||
extra={
|
|
||||||
<Space wrap>
|
|
||||||
<Button onClick={() => refetch()}>
|
|
||||||
<SyncOutlined/>
|
|
||||||
</Button>
|
|
||||||
{job && job.converted ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setBillEnterContext({
|
|
||||||
actions: {refetch: billsQuery.refetch},
|
|
||||||
context: {
|
|
||||||
job,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("jobs.actions.postbills")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setReconciliationContext({
|
|
||||||
actions: {refetch: billsQuery.refetch},
|
|
||||||
context: {
|
|
||||||
job,
|
|
||||||
bills: (billsQuery.data && billsQuery.data.bills) || [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("jobs.actions.reconcile")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Input.Search
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSearchText(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
loading={billsQuery.loading}
|
|
||||||
scroll={{
|
|
||||||
x: true, // y: "50rem"
|
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
>
|
||||||
rowKey="id"
|
{t("jobs.actions.postbills")}
|
||||||
dataSource={filteredBills}
|
</Button>
|
||||||
onChange={handleTableChange}
|
<Button
|
||||||
/>
|
onClick={() => {
|
||||||
</Card>
|
setReconciliationContext({
|
||||||
);
|
actions: { refetch: billsQuery.refetch },
|
||||||
|
context: {
|
||||||
|
job,
|
||||||
|
bills: (billsQuery.data && billsQuery.data.bills) || []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.reconcile")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={billsQuery.loading}
|
||||||
|
scroll={{
|
||||||
|
x: true // y: "50rem"
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={filteredBills}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(BillsListTableComponent);
|
|
||||||
|
|||||||
@@ -1,121 +1,112 @@
|
|||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
import {useQuery} from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import {Input, Table} from "antd";
|
import { Input, Table } from "antd";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {alphaSort} from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
export default function BillsVendorsList() {
|
export default function BillsVendorsList() {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|
||||||
const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, {
|
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: "",
|
search: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("vendors.fields.name"),
|
title: t("vendors.fields.name"),
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
sorter: (a, b) => alphaSort(a.name, b.name),
|
sorter: (a, b) => alphaSort(a.name, b.name),
|
||||||
sortOrder:
|
sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order
|
||||||
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
|
},
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.cost_center"),
|
||||||
|
dataIndex: "cost_center",
|
||||||
|
key: "cost_center",
|
||||||
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vendors.fields.city"),
|
||||||
|
dataIndex: "city",
|
||||||
|
key: "city"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleOnRowClick = (record) => {
|
||||||
|
if (record) {
|
||||||
|
delete search.billid;
|
||||||
|
if (record.id) {
|
||||||
|
search.vendorid = record.id;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete search.vendorid;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
setState({ ...state, search: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
const dataSource = state.search
|
||||||
|
? data.vendors.filter(
|
||||||
|
(v) =>
|
||||||
|
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
(v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||||
|
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
|
)
|
||||||
|
: (data && data.vendors) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
title={() => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
dataSource={dataSource}
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (record) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
},
|
},
|
||||||
{
|
selectedRowKeys: [search.vendorid],
|
||||||
title: t("vendors.fields.cost_center"),
|
type: "radio"
|
||||||
dataIndex: "cost_center",
|
}}
|
||||||
key: "cost_center",
|
onRow={(record, rowIndex) => {
|
||||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
return {
|
||||||
sortOrder:
|
onClick: (event) => {
|
||||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
handleOnRowClick(record);
|
||||||
},
|
} // click row
|
||||||
{
|
};
|
||||||
title: t("vendors.fields.city"),
|
}}
|
||||||
dataIndex: "city",
|
/>
|
||||||
key: "city",
|
);
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
|
||||||
if (record) {
|
|
||||||
delete search.billid;
|
|
||||||
if (record.id) {
|
|
||||||
search.vendorid = record.id;
|
|
||||||
history.push({search: queryString.stringify(search)});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete search.vendorid;
|
|
||||||
history.push({search: queryString.stringify(search)});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
|
||||||
setState({...state, search: e.target.value});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
|
||||||
|
|
||||||
const dataSource = state.search
|
|
||||||
? data.vendors.filter(
|
|
||||||
(v) =>
|
|
||||||
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
|
||||||
(v.cost_center || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
|
|
||||||
)
|
|
||||||
: (data && data.vendors) || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
loading={loading}
|
|
||||||
title={() => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
value={state.search}
|
|
||||||
onChange={handleSearch}
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
dataSource={dataSource}
|
|
||||||
pagination={{position: "top"}}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
onChange={handleTableChange}
|
|
||||||
rowSelection={{
|
|
||||||
onSelect: (record) => {
|
|
||||||
handleOnRowClick(record);
|
|
||||||
},
|
|
||||||
selectedRowKeys: [search.vendorid],
|
|
||||||
type: "radio",
|
|
||||||
}}
|
|
||||||
onRow={(record, rowIndex) => {
|
|
||||||
return {
|
|
||||||
onClick: (event) => {
|
|
||||||
handleOnRowClick(record);
|
|
||||||
}, // click row
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,63 @@
|
|||||||
import {HomeFilled} from "@ant-design/icons";
|
import { HomeFilled } from "@ant-design/icons";
|
||||||
import {Breadcrumb, Col, Row} from "antd";
|
import { Breadcrumb, Col, Row } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {Link} from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
|
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||||
import "./breadcrumbs.styles.scss";
|
import "./breadcrumbs.styles.scss";
|
||||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
breadcrumbs: selectBreadcrumbs,
|
breadcrumbs: selectBreadcrumbs,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BreadCrumbs({breadcrumbs, bodyshop}) {
|
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
||||||
|
const {
|
||||||
const {treatments: {OpenSearch}} = useSplitTreatments({
|
treatments: { OpenSearch }
|
||||||
attributes: {},
|
} = useSplitTreatments({
|
||||||
names: ["OpenSearch"],
|
attributes: {},
|
||||||
splitKey: bodyshop && bodyshop.imexshopid,
|
names: ["OpenSearch"],
|
||||||
});
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
// TODO - Client Update - Technically key is not doing anything here
|
});
|
||||||
return (
|
// TODO - Client Update - Technically key is not doing anything here
|
||||||
<Row className="breadcrumb-container">
|
return (
|
||||||
<Col xs={24} sm={24} md={16}>
|
<Row className="breadcrumb-container">
|
||||||
<Breadcrumb
|
<Col xs={24} sm={24} md={16}>
|
||||||
separator=">"
|
<Breadcrumb
|
||||||
items={[
|
separator=">"
|
||||||
{
|
items={[
|
||||||
key: "home",
|
{
|
||||||
title: (
|
key: "home",
|
||||||
<Link to={`/manage/`}>
|
title: (
|
||||||
<HomeFilled/>{" "}
|
<Link to={`/manage/`}>
|
||||||
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
|
||||||
""}
|
</Link>
|
||||||
</Link>
|
)
|
||||||
),
|
},
|
||||||
},
|
...breadcrumbs.map((item) =>
|
||||||
...breadcrumbs.map((item) =>
|
item.link
|
||||||
item.link
|
? {
|
||||||
? {
|
key: item.label,
|
||||||
key: item.label,
|
title: <Link to={item.link}>{item.label}</Link>
|
||||||
title: <Link to={item.link}>{item.label}</Link>,
|
}
|
||||||
}
|
: {
|
||||||
: {
|
key: item.label,
|
||||||
key: item.label,
|
title: item.label
|
||||||
title: item.label,
|
}
|
||||||
}
|
)
|
||||||
),
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
</Col>
|
||||||
</Col>
|
<Col xs={24} sm={24} md={8}>
|
||||||
<Col xs={24} sm={24} md={8}>
|
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
|
||||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||||
|
|||||||
@@ -1,99 +1,87 @@
|
|||||||
import {Button, Form, Modal} from "antd";
|
import { Button, Form, Modal } from "antd";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors";
|
import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
|
||||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import {TemplateList} from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
|
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
caBcEtfTableModal: selectCaBcEtfTableConvert,
|
caBcEtfTableModal: selectCaBcEtfTableConvert
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () =>
|
toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
|
||||||
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ContractsFindModalContainer({
|
export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
|
||||||
caBcEtfTableModal,
|
const { t } = useTranslation();
|
||||||
toggleModalVisible,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
const {open} = caBcEtfTableModal;
|
const { open } = caBcEtfTableModal;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("ca_bc_etf_table_parse");
|
logImEXEvent("ca_bc_etf_table_parse");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const claimNumbers = [];
|
const claimNumbers = [];
|
||||||
values.table.split("\n").forEach((row, idx, arr) => {
|
values.table.split("\n").forEach((row, idx, arr) => {
|
||||||
const {1: claim, 2: shortclaim, 4: amount} = row.split("\t");
|
const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
|
||||||
if (!claim || !shortclaim) return;
|
if (!claim || !shortclaim) return;
|
||||||
const trimmedShortClaim = shortclaim.trim();
|
const trimmedShortClaim = shortclaim.trim();
|
||||||
// const trimmedClaim = claim.trim();
|
// const trimmedClaim = claim.trim();
|
||||||
if (amount.slice(-1) === "-") {
|
if (amount.slice(-1) === "-") {
|
||||||
}
|
}
|
||||||
|
|
||||||
claimNumbers.push({
|
claimNumbers.push({
|
||||||
claim: trimmedShortClaim,
|
claim: trimmedShortClaim,
|
||||||
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount,
|
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await GenerateDocument(
|
await GenerateDocument(
|
||||||
{
|
{
|
||||||
name: EtfTemplate.key,
|
name: EtfTemplate.key,
|
||||||
variables: {
|
variables: {
|
||||||
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
|
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
|
||||||
claimdata: claimNumbers,
|
claimdata: claimNumbers
|
||||||
},
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
values.sendby === "email" ? "e" : "p"
|
|
||||||
);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
form.resetFields();
|
|
||||||
}
|
}
|
||||||
}, [open, form]);
|
},
|
||||||
|
{},
|
||||||
return (
|
values.sendby === "email" ? "e" : "p"
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
width="70%"
|
|
||||||
title={t("payments.labels.findermodal")}
|
|
||||||
onCancel={() => toggleModalVisible()}
|
|
||||||
onOk={() => toggleModalVisible()}
|
|
||||||
destroyOnClose
|
|
||||||
forceRender
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
autoComplete="no"
|
|
||||||
onFinish={handleFinish}
|
|
||||||
>
|
|
||||||
<CaBcEtfTableModalComponent form={form}/>
|
|
||||||
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
|
||||||
{t("general.labels.search")}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
width="70%"
|
||||||
|
title={t("payments.labels.findermodal")}
|
||||||
|
onCancel={() => toggleModalVisible()}
|
||||||
|
onOk={() => toggleModalVisible()}
|
||||||
|
destroyOnClose
|
||||||
|
forceRender
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
<CaBcEtfTableModalComponent form={form} />
|
||||||
|
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
||||||
|
{t("general.labels.search")}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ContractsFindModalContainer);
|
|
||||||
|
|||||||
@@ -1,42 +1,38 @@
|
|||||||
import {Form, Input, Radio} from "antd";
|
import { Form, Input, Radio } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
|
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
|
||||||
|
|
||||||
export function PartsReceiveModalComponent({bodyshop, form}) {
|
export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="table"
|
name="table"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={8}/>
|
<Input.TextArea rows={8} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
|
||||||
label={t("general.labels.sendby")}
|
<Radio.Group>
|
||||||
name="sendby"
|
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||||
initialValue="print"
|
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||||
>
|
</Radio.Group>
|
||||||
<Radio.Group>
|
</Form.Item>
|
||||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
</div>
|
||||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
);
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,45 @@
|
|||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {Button, Form, InputNumber, Popover} from "antd";
|
import { Button, Form, InputNumber, Popover } from "antd";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {CalculatorFilled} from "@ant-design/icons";
|
import { CalculatorFilled } from "@ant-design/icons";
|
||||||
|
|
||||||
export default function CABCpvrtCalculator({disabled, form}) {
|
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2)
|
||||||
});
|
});
|
||||||
form.setFields([{name: "ca_bc_pvrt", touched: true}]);
|
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Form onFinish={handleFinish} initialValues={{rate: 1.5}}>
|
<Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}>
|
||||||
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
||||||
<InputNumber precision={2} min={0}/>
|
<InputNumber precision={2} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||||
<InputNumber precision={0} min={0}/>
|
<InputNumber precision={0} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
{t("general.actions.calculate")}
|
{t("general.actions.calculate")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
||||||
destroyTooltipOnHide
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
content={popContent}
|
<CalculatorFilled />
|
||||||
open={visibility}
|
</Button>
|
||||||
disabled={disabled}
|
</Popover>
|
||||||
>
|
);
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
|
||||||
<CalculatorFilled/>
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,357 +1,328 @@
|
|||||||
import {DeleteFilled} from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import {useLazyQuery, useMutation} from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd";
|
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries";
|
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||||
import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries";
|
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import {selectCardPayment} from "../../redux/modals/modals.selectors";
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
cardPaymentModal: selectCardPayment,
|
cardPaymentModal: selectCardPayment,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({jobid, operation, type}) =>
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||||
dispatch(insertAuditTrail({jobid, operation, type})),
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const CardPaymentModalComponent = ({
|
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||||
bodyshop,
|
const { context } = cardPaymentModal;
|
||||||
cardPaymentModal,
|
|
||||||
toggleModalVisible,
|
|
||||||
insertAuditTrail,
|
|
||||||
}) => {
|
|
||||||
const {context} = cardPaymentModal;
|
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [, {data, refetch, queryLoading}] = useLazyQuery(
|
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
variables: { jobids: [context.jobid] },
|
||||||
{
|
skip: true
|
||||||
variables: {jobids: [context.jobid]},
|
});
|
||||||
skip: true,
|
|
||||||
|
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||||
|
//Initialize the intellipay window.
|
||||||
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
|
window.intellipay.runOnClose(() => {
|
||||||
|
//window.intellipay.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.intellipay.runOnApproval(async function (response) {
|
||||||
|
console.warn("*** Running On Approval Script ***");
|
||||||
|
form.setFieldValue("paymentResponse", response);
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.intellipay.runOnNonApproval(async function (response) {
|
||||||
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
|
||||||
|
await insertPaymentResponse({
|
||||||
|
variables: {
|
||||||
|
paymentResponse: payments.map((payment) => ({
|
||||||
|
amount: payment.amount,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: payment.jobid,
|
||||||
|
declinereason: response.declinereason,
|
||||||
|
ext_paymentid: response.paymentid.toString(),
|
||||||
|
successful: false,
|
||||||
|
response
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
payments.forEach((payment) =>
|
||||||
//Initialize the intellipay window.
|
insertAuditTrail({
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
jobid: payment.jobid,
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
operation: AuditTrailMapping.failedpayment(),
|
||||||
window.intellipay.runOnClose(() => {
|
type: "failedpayment"
|
||||||
//window.intellipay.initialize();
|
})
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
const handleFinish = async (values) => {
|
||||||
console.warn("*** Running On Approval Script ***");
|
try {
|
||||||
form.setFieldValue("paymentResponse", response);
|
await insertPayment({
|
||||||
form.submit();
|
variables: {
|
||||||
});
|
paymentInput: values.payments.map((payment) => ({
|
||||||
|
amount: payment.amount,
|
||||||
|
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||||
|
payer: t("payments.labels.customer"),
|
||||||
|
type: values.paymentResponse.cardbrand,
|
||||||
|
jobid: payment.jobid,
|
||||||
|
date: dayjs(Date.now()),
|
||||||
|
payment_responses: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
amount: payment.amount,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
jobid: payment.jobid,
|
||||||
// Mutate unsuccessful payment
|
declinereason: values.paymentResponse.declinereason,
|
||||||
|
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||||
const {payments} = form.getFieldsValue();
|
successful: true,
|
||||||
|
response: values.paymentResponse
|
||||||
await insertPaymentResponse({
|
}
|
||||||
variables: {
|
]
|
||||||
paymentResponse: payments.map((payment) => ({
|
|
||||||
amount: payment.amount,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
jobid: payment.jobid,
|
|
||||||
declinereason: response.declinereason,
|
|
||||||
ext_paymentid: response.paymentid.toString(),
|
|
||||||
successful: false,
|
|
||||||
response,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
payments.forEach((payment) =>
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: payment.jobid,
|
|
||||||
operation: AuditTrailMapping.failedpayment(),
|
|
||||||
type: "failedpayment",})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
try {
|
|
||||||
await insertPayment({
|
|
||||||
variables: {
|
|
||||||
paymentInput: values.payments.map((payment) => ({
|
|
||||||
amount: payment.amount,
|
|
||||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
|
||||||
payer: t("payments.labels.customer"),
|
|
||||||
type: values.paymentResponse.cardbrand,
|
|
||||||
jobid: payment.jobid,
|
|
||||||
date: dayjs(Date.now()),
|
|
||||||
payment_responses: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
amount: payment.amount,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
|
|
||||||
jobid: payment.jobid,
|
|
||||||
declinereason: values.paymentResponse.declinereason,
|
|
||||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
|
||||||
successful: true,
|
|
||||||
response: values.paymentResponse,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
refetchQueries: ["GET_JOB_BY_PK"],
|
|
||||||
});
|
|
||||||
toggleModalVisible();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("payments.errors.inserting", {error: error.message}),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIntelliPayCharge = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
//Validate
|
|
||||||
try {
|
|
||||||
await form.validateFields();
|
|
||||||
} catch (error) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
|
||||||
bodyshop,
|
|
||||||
refresh: !!window.intellipay,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.intellipay) {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
eval(response.data);
|
|
||||||
SetIntellipayCallbackFunctions();
|
|
||||||
window.intellipay.autoOpen();
|
|
||||||
} else {
|
|
||||||
var rg = document.createRange();
|
|
||||||
let node = rg.createContextualFragment(response.data);
|
|
||||||
document.documentElement.appendChild(node);
|
|
||||||
SetIntellipayCallbackFunctions();
|
|
||||||
window.intellipay.isAutoOpen = true;
|
|
||||||
window.intellipay.initialize();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}))
|
||||||
notification.open({
|
},
|
||||||
type: "error",
|
refetchQueries: ["GET_JOB_BY_PK"]
|
||||||
message: t("job_payments.notifications.error.openingip"),
|
});
|
||||||
});
|
toggleModalVisible();
|
||||||
setLoading(false);
|
} catch (error) {
|
||||||
}
|
console.error(error);
|
||||||
};
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("payments.errors.inserting", { error: error.message })
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleIntelliPayCharge = async () => {
|
||||||
<Card title="Card Payment">
|
setLoading(true);
|
||||||
<Spin spinning={loading}>
|
|
||||||
<Form
|
|
||||||
onFinish={handleFinish}
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{
|
|
||||||
payments: context.jobid ? [{jobid: context.jobid}] : [],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.List name={["payments"]}>
|
|
||||||
{(fields, {add, remove, move}) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item key={field.key}>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col span={16}>
|
|
||||||
<Form.Item
|
|
||||||
key={`${index}jobid`}
|
|
||||||
label={t("jobs.fields.ro_number")}
|
|
||||||
name={[field.name, "jobid"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<JobSearchSelectComponent
|
|
||||||
notExported={false}
|
|
||||||
clm_no
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
key={`${index}amount`}
|
|
||||||
label={t("payments.fields.amount")}
|
|
||||||
name={[field.name, "amount"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyFormItemComponent/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<DeleteFilled
|
|
||||||
style={{margin: "1rem"}}
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{width: "100%"}}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
|
|
||||||
<Form.Item
|
//Validate
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
try {
|
||||||
prevValues.payments?.map((p) => p?.jobid).join() !==
|
await form.validateFields();
|
||||||
curValues.payments?.map((p) => p?.jobid).join()
|
} catch (error) {
|
||||||
}
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
|
bodyshop,
|
||||||
|
refresh: !!window.intellipay
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.intellipay) {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
eval(response.data);
|
||||||
|
SetIntellipayCallbackFunctions();
|
||||||
|
window.intellipay.autoOpen();
|
||||||
|
} else {
|
||||||
|
var rg = document.createRange();
|
||||||
|
let node = rg.createContextualFragment(response.data);
|
||||||
|
document.documentElement.appendChild(node);
|
||||||
|
SetIntellipayCallbackFunctions();
|
||||||
|
window.intellipay.isAutoOpen = true;
|
||||||
|
window.intellipay.initialize();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("job_payments.notifications.error.openingip")
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Card Payment">
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
onFinish={handleFinish}
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
payments: context.jobid ? [{ jobid: context.jobid }] : []
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.List name={["payments"]}>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.Item
|
||||||
|
key={`${index}jobid`}
|
||||||
|
label={t("jobs.fields.ro_number")}
|
||||||
|
name={[field.name, "jobid"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<JobSearchSelectComponent notExported={false} clm_no />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
key={`${index}amount`}
|
||||||
|
label={t("payments.fields.amount")}
|
||||||
|
name={[field.name, "amount"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<DeleteFilled
|
||||||
|
style={{ margin: "1rem" }}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
{() => {
|
{t("general.actions.add")}
|
||||||
console.log("Updating the owner info section.");
|
</Button>
|
||||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
</Form.Item>
|
||||||
const {payments} = form.getFieldsValue();
|
</div>
|
||||||
if (
|
);
|
||||||
payments?.length > 0 &&
|
}}
|
||||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
</Form.List>
|
||||||
) {
|
|
||||||
console.log("**Calling refetch.");
|
|
||||||
refetch({jobids: payments.map((p) => p.jobid)});
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
"Acc info",
|
|
||||||
data,
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="account"
|
|
||||||
type="hidden"
|
|
||||||
value={
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="email"
|
|
||||||
type="hidden"
|
|
||||||
value={
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
|
||||||
prevValues.payments?.map((p) => p?.amount).join() !==
|
|
||||||
curValues.payments?.map((p) => p?.amount).join()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const {payments} = form.getFieldsValue();
|
|
||||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
|
||||||
return acc + (val?.amount || 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return (
|
<Form.Item
|
||||||
<Space style={{float: "right"}}>
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
<Statistic
|
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
||||||
title="Amount To Charge"
|
}
|
||||||
value={totalAmountToCharge}
|
>
|
||||||
precision={2}
|
{() => {
|
||||||
/>
|
console.log("Updating the owner info section.");
|
||||||
<Input
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||||
className="ipayfield"
|
const { payments } = form.getFieldsValue();
|
||||||
data-ipayname="amount"
|
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||||
type="hidden"
|
console.log("**Calling refetch.");
|
||||||
value={totalAmountToCharge?.toFixed(2)}
|
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||||
/>
|
}
|
||||||
<Button
|
console.log(
|
||||||
type="primary"
|
"Acc info",
|
||||||
// data-ipayname="submit"
|
data,
|
||||||
className="ipayfield"
|
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||||
loading={queryLoading || loading}
|
);
|
||||||
disabled={!(totalAmountToCharge > 0)}
|
return (
|
||||||
onClick={handleIntelliPayCharge}
|
<>
|
||||||
>
|
<Input
|
||||||
{t("job_payments.buttons.proceedtopayment")}
|
className="ipayfield"
|
||||||
</Button>
|
data-ipayname="account"
|
||||||
</Space>
|
type="hidden"
|
||||||
);
|
value={
|
||||||
}}
|
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||||
</Form.Item>
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="email"
|
||||||
|
type="hidden"
|
||||||
|
value={
|
||||||
|
payments && data && data.jobs.length > 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
|
prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||||
|
return acc + (val?.amount || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
{/* Lightbox payment response when it is completed */}
|
return (
|
||||||
<Form.Item name="paymentResponse" hidden>
|
<Space style={{ float: "right" }}>
|
||||||
<Input type="hidden"/>
|
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||||
</Form.Item>
|
<Input
|
||||||
</Form>
|
className="ipayfield"
|
||||||
</Spin>
|
data-ipayname="amount"
|
||||||
</Card>
|
type="hidden"
|
||||||
);
|
value={totalAmountToCharge?.toFixed(2)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
// data-ipayname="submit"
|
||||||
|
className="ipayfield"
|
||||||
|
loading={queryLoading || loading}
|
||||||
|
disabled={!(totalAmountToCharge > 0)}
|
||||||
|
onClick={handleIntelliPayCharge}
|
||||||
|
>
|
||||||
|
{t("job_payments.buttons.proceedtopayment")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Lightbox payment response when it is completed */}
|
||||||
|
<Form.Item name="paymentResponse" hidden>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(CardPaymentModalComponent);
|
|
||||||
|
|||||||
@@ -1,57 +1,50 @@
|
|||||||
import {Button, Modal} from "antd";
|
import { Button, Modal } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import {selectCardPayment} from "../../redux/modals/modals.selectors";
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CardPaymentModalComponent from "./card-payment-modal.component.";
|
import CardPaymentModalComponent from "./card-payment-modal.component.";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
cardPaymentModal: selectCardPayment,
|
cardPaymentModal: selectCardPayment,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||||
});
|
});
|
||||||
|
|
||||||
function CardPaymentModalContainer({
|
function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
|
||||||
cardPaymentModal,
|
const { open } = cardPaymentModal;
|
||||||
toggleModalVisible,
|
const { t } = useTranslation();
|
||||||
bodyshop,
|
|
||||||
}) {
|
|
||||||
const {open} = cardPaymentModal;
|
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOK = () => {
|
const handleOK = () => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onOk={handleOK}
|
onOk={handleOK}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
footer={[
|
footer={[
|
||||||
<Button key="back" onClick={handleCancel}>
|
<Button key="back" onClick={handleCancel}>
|
||||||
{t("job_payments.buttons.goback")}
|
{t("job_payments.buttons.goback")}
|
||||||
</Button>,
|
</Button>
|
||||||
]}
|
]}
|
||||||
width="80%"
|
width="80%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<CardPaymentModalComponent/>
|
<CardPaymentModalComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(CardPaymentModalContainer);
|
|
||||||
|
|||||||
@@ -1,100 +1,97 @@
|
|||||||
import {useApolloClient} from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import {getToken, onMessage} from "@firebase/messaging";
|
import { getToken, onMessage } from "@firebase/messaging";
|
||||||
import {Button, notification, Space} from "antd";
|
import { Button, notification, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, {useEffect} from "react";
|
import React, { useEffect } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {messaging, requestForToken} from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import FcmHandler from "../../utils/fcm-handler";
|
import FcmHandler from "../../utils/fcm-handler";
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
|
|
||||||
export function ChatAffixContainer({bodyshop, chatVisible}) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
|
||||||
|
|
||||||
async function SubscribeToTopic() {
|
useEffect(() => {
|
||||||
try {
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
|
||||||
fcm_tokens: await getToken(messaging, {
|
|
||||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
|
|
||||||
}),
|
|
||||||
type: "messaging",
|
|
||||||
imexshopid: bodyshop.imexshopid,
|
|
||||||
});
|
|
||||||
console.log("FCM Topic Subscription", r.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"Error attempting to subscribe to messaging topic: ",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
notification.open({
|
|
||||||
key: 'fcm',
|
|
||||||
type: "warning",
|
|
||||||
message: t("general.errors.fcm"),
|
|
||||||
btn: (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await requestForToken();
|
|
||||||
SubscribeToTopic();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.tryagain")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const win = window.open(
|
|
||||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
win.focus();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.labels.help")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscribeToTopic();
|
async function SubscribeToTopic() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
try {
|
||||||
}, [bodyshop]);
|
const r = await axios.post("/notifications/subscribe", {
|
||||||
|
fcm_tokens: await getToken(messaging, {
|
||||||
|
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
||||||
|
}),
|
||||||
|
type: "messaging",
|
||||||
|
imexshopid: bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
console.log("FCM Topic Subscription", r.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error attempting to subscribe to messaging topic: ", error);
|
||||||
|
notification.open({
|
||||||
|
key: "fcm",
|
||||||
|
type: "warning",
|
||||||
|
message: t("general.errors.fcm"),
|
||||||
|
btn: (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await requestForToken();
|
||||||
|
SubscribeToTopic();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.actions.tryagain")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const win = window.open(
|
||||||
|
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
win.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.labels.help")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
SubscribeToTopic();
|
||||||
function handleMessage(payload) {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
FcmHandler({
|
}, [bodyshop]);
|
||||||
client,
|
|
||||||
payload: (payload && payload.data && payload.data.data) || payload.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let stopMessageListener, channel;
|
useEffect(() => {
|
||||||
try {
|
function handleMessage(payload) {
|
||||||
stopMessageListener = onMessage(messaging, handleMessage);
|
FcmHandler({
|
||||||
channel = new BroadcastChannel("imex-sw-messages");
|
client,
|
||||||
channel.addEventListener("message", handleMessage);
|
payload: (payload && payload.data && payload.data.data) || payload.data
|
||||||
} catch (error) {
|
});
|
||||||
console.log("Unable to set event listeners.");
|
}
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
stopMessageListener && stopMessageListener();
|
|
||||||
channel && channel.removeEventListener("message", handleMessage);
|
|
||||||
};
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
let stopMessageListener, channel;
|
||||||
|
try {
|
||||||
|
stopMessageListener = onMessage(messaging, handleMessage);
|
||||||
|
channel = new BroadcastChannel("imex-sw-messages");
|
||||||
|
channel.addEventListener("message", handleMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Unable to set event listeners.");
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
stopMessageListener && stopMessageListener();
|
||||||
|
channel && channel.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
return (
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
|
||||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
|
return (
|
||||||
</div>
|
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
);
|
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatAffixContainer;
|
export default ChatAffixContainer;
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {Button} from "antd";
|
import { Button } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {TOGGLE_CONVERSATION_ARCHIVE} from "../../graphql/conversations.queries";
|
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||||
|
|
||||||
export default function ChatArchiveButton({conversation}) {
|
export default function ChatArchiveButton({ conversation }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||||
const handleToggleArchive = async () => {
|
const handleToggleArchive = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await updateConversation({
|
await updateConversation({
|
||||||
variables: {id: conversation.id, archived: !conversation.archived},
|
variables: { id: conversation.id, archived: !conversation.archived },
|
||||||
refetchQueries: ["CONVERSATION_LIST_QUERY"],
|
refetchQueries: ["CONVERSATION_LIST_QUERY"]
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
||||||
{conversation.archived
|
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
||||||
? t("messaging.labels.unarchive")
|
</Button>
|
||||||
: t("messaging.labels.archive")}
|
);
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,101 @@
|
|||||||
import {Badge, Card, List, Space, Tag} from "antd";
|
import { Badge, Card, List, Space, Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
|
import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
|
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import {TimeAgoFormatter} from "../../utils/DateFormatter";
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setSelectedConversation: (conversationId) =>
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
dispatch(setSelectedConversation(conversationId)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({
|
function ChatConversationListComponent({
|
||||||
conversationList,
|
conversationList,
|
||||||
selectedConversation,
|
selectedConversation,
|
||||||
setSelectedConversation,
|
setSelectedConversation,
|
||||||
loadMoreConversations,
|
loadMoreConversations
|
||||||
}) {
|
}) {
|
||||||
const cache = new CellMeasurerCache({
|
const cache = new CellMeasurerCache({
|
||||||
fixedWidth: true,
|
fixedWidth: true,
|
||||||
defaultHeight: 60,
|
defaultHeight: 60
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowRenderer = ({index, key, style, parent}) => {
|
const rowRenderer = ({ index, key, style, parent }) => {
|
||||||
const item = conversationList[index];
|
const item = conversationList[index];
|
||||||
const cardContentRight =
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentLeft =
|
||||||
const cardContentLeft = item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
? item.job_conversations.map((j, idx) => (
|
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
||||||
<Tag key={idx}>{j.job.ro_number}</Tag>
|
: null;
|
||||||
))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
|
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
||||||
OwnerNameDisplayFunction(j.job)
|
|
||||||
))}</>
|
|
||||||
|
|
||||||
const cardTitle = <>
|
const cardTitle = (
|
||||||
{item.label && <Tag color="blue">{item.label}</Tag>}
|
<>
|
||||||
{item.job_conversations.length > 0 ? (
|
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||||
<Space direction="vertical">
|
{item.job_conversations.length > 0 ? (
|
||||||
{names}
|
<Space direction="vertical">{names}</Space>
|
||||||
</Space>
|
) : (
|
||||||
) : (
|
<Space>
|
||||||
<Space>
|
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
</Space>
|
||||||
</Space>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
);
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
? {backgroundColor: 'rgba(128, 128, 128, 0.2)'}
|
? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
|
||||||
: {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'};
|
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||||
|
|
||||||
return (
|
|
||||||
<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
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
||||||
<AutoSizer>
|
<List.Item
|
||||||
{({height, width}) => (
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
<VirtualizedList
|
style={style}
|
||||||
height={height}
|
className={`chat-list-item
|
||||||
width={width}
|
${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
|
||||||
rowCount={conversationList.length}
|
>
|
||||||
rowHeight={cache.rowHeight}
|
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
rowRenderer={rowRenderer}
|
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||||
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
if (scrollTop + clientHeight === scrollHeight) {
|
</Card>
|
||||||
loadMoreConversations();
|
</List.Item>
|
||||||
}
|
</CellMeasurer>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-list-container">
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<VirtualizedList
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
rowCount={conversationList.length}
|
||||||
|
rowHeight={cache.rowHeight}
|
||||||
|
rowRenderer={rowRenderer}
|
||||||
|
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
||||||
|
if (scrollTop + clientHeight === scrollHeight) {
|
||||||
|
loadMoreConversations();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationListComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ChatConversationListComponent);
|
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {Tag} from "antd";
|
import { Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {REMOVE_CONVERSATION_TAG} from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
|
||||||
export default function ChatConversationTitleTags({jobConversations}) {
|
export default function ChatConversationTitleTags({ jobConversations }) {
|
||||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||||
|
|
||||||
const handleRemoveTag = (jobId) => {
|
const handleRemoveTag = (jobId) => {
|
||||||
const convId = jobConversations[0].conversationid;
|
const convId = jobConversations[0].conversationid;
|
||||||
if (!!convId) {
|
if (!!convId) {
|
||||||
removeJobConversation({
|
removeJobConversation({
|
||||||
variables: {
|
variables: {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId,
|
jobId: jobId
|
||||||
},
|
},
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: cache.identify({id: convId, __typename: "conversations"}),
|
id: cache.identify({ id: convId, __typename: "conversations" }),
|
||||||
fields: {
|
fields: {
|
||||||
job_conversations(ex) {
|
job_conversations(ex) {
|
||||||
return ex.filter((e) => e.jobid !== jobId);
|
return ex.filter((e) => e.jobid !== jobId);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
logImEXEvent("messaging_remove_job_tag", {
|
|
||||||
conversationId: convId,
|
|
||||||
jobId: jobId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
logImEXEvent("messaging_remove_job_tag", {
|
||||||
|
conversationId: convId,
|
||||||
|
jobId: jobId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{jobConversations.map((item) => (
|
{jobConversations.map((item) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={item.job.id}
|
key={item.job.id}
|
||||||
closable
|
closable
|
||||||
color="blue"
|
color="blue"
|
||||||
style={{cursor: "pointer"}}
|
style={{ cursor: "pointer" }}
|
||||||
onClose={() => handleRemoveTag(item.job.id)}
|
onClose={() => handleRemoveTag(item.job.id)}
|
||||||
>
|
>
|
||||||
<Link to={`/manage/jobs/${item.job.id}`}>
|
<Link to={`/manage/jobs/${item.job.id}`}>
|
||||||
{`${item.job.ro_number || "?"} | `}
|
{`${item.job.ro_number || "?"} | `}
|
||||||
<OwnerNameDisplay ownerObject={item.job}/>
|
<OwnerNameDisplay ownerObject={item.job} />
|
||||||
</Link>
|
</Link>
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Space} from "antd";
|
import { Space } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||||
@@ -7,21 +7,15 @@ import ChatLabelComponent from "../chat-label/chat-label.component";
|
|||||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
|
||||||
export default function ChatConversationTitle({conversation}) {
|
export default function ChatConversationTitle({ conversation }) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PhoneNumberFormatter>
|
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
||||||
{conversation && conversation.phone_num}
|
<ChatLabelComponent conversation={conversation} />
|
||||||
</PhoneNumberFormatter>
|
<ChatPrintButton conversation={conversation} />
|
||||||
<ChatLabelComponent conversation={conversation}/>
|
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
|
||||||
<ChatPrintButton conversation={conversation}/>
|
<ChatTagRoContainer conversation={conversation || []} />
|
||||||
<ChatConversationTitleTags
|
<ChatArchiveButton conversation={conversation} />
|
||||||
jobConversations={
|
</Space>
|
||||||
(conversation && conversation.job_conversations) || []
|
);
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ChatTagRoContainer conversation={conversation || []}/>
|
|
||||||
<ChatArchiveButton conversation={conversation}/>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,21 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||||
import "./chat-conversation.styles.scss";
|
import "./chat-conversation.styles.scss";
|
||||||
|
|
||||||
export default function ChatConversationComponent({
|
export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
|
||||||
subState,
|
const [loading, error] = subState;
|
||||||
conversation,
|
|
||||||
messages,
|
|
||||||
handleMarkConversationAsRead,
|
|
||||||
}) {
|
|
||||||
const [loading, error] = subState;
|
|
||||||
|
|
||||||
if (loading) return <LoadingSkeleton/>;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="chat-conversation"
|
className="chat-conversation"
|
||||||
onMouseDown={handleMarkConversationAsRead}
|
onMouseDown={handleMarkConversationAsRead}
|
||||||
onKeyDown={handleMarkConversationAsRead}
|
onKeyDown={handleMarkConversationAsRead}
|
||||||
>
|
>
|
||||||
<ChatConversationTitle conversation={conversation}/>
|
<ChatConversationTitle conversation={conversation} />
|
||||||
<ChatMessageListComponent messages={messages}/>
|
<ChatMessageListComponent messages={messages} />
|
||||||
<ChatSendMessage conversation={conversation}/>
|
<ChatSendMessage conversation={conversation} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,81 @@
|
|||||||
import {useMutation, useQuery, useSubscription} from "@apollo/client";
|
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries";
|
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
import {MARK_MESSAGES_AS_READ_BY_CONVERSATION} from "../../graphql/messages.queries";
|
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||||
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
||||||
|
|
||||||
export function ChatConversationContainer({bodyshop, selectedConversation}) {
|
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||||
const {
|
const {
|
||||||
loading: convoLoading,
|
loading: convoLoading,
|
||||||
error: convoError,
|
error: convoError,
|
||||||
data: convoData,
|
data: convoData
|
||||||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||||||
variables: {conversationId: selectedConversation},
|
variables: { conversationId: selectedConversation },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const {loading, error, data} = useSubscription(
|
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
variables: { conversationId: selectedConversation }
|
||||||
{
|
});
|
||||||
variables: {conversationId: selectedConversation},
|
|
||||||
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
|
const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
||||||
|
variables: { conversationId: selectedConversation },
|
||||||
|
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
id: cache.identify({
|
||||||
|
__typename: "conversations",
|
||||||
|
id: selectedConversation
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
messages_aggregate(cached) {
|
||||||
|
return { aggregate: { count: 0 } };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
const unreadCount =
|
||||||
|
data &&
|
||||||
|
data.messages &&
|
||||||
|
data.messages.reduce((acc, val) => {
|
||||||
|
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
const [markConversationRead] = useMutation(
|
const handleMarkConversationAsRead = async () => {
|
||||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||||
{
|
setMarkingAsReadInProgress(true);
|
||||||
variables: {conversationId: selectedConversation},
|
await markConversationRead({});
|
||||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
await axios.post("/sms/markConversationRead", {
|
||||||
update(cache) {
|
conversationid: selectedConversation,
|
||||||
cache.modify({
|
imexshopid: bodyshop.imexshopid
|
||||||
id: cache.identify({
|
});
|
||||||
__typename: "conversations",
|
setMarkingAsReadInProgress(false);
|
||||||
id: selectedConversation,
|
}
|
||||||
}),
|
};
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
|
||||||
return {aggregate: {count: 0}};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const unreadCount =
|
return (
|
||||||
data &&
|
<ChatConversationComponent
|
||||||
data.messages &&
|
subState={[loading || convoLoading, error || convoError]}
|
||||||
data.messages.reduce((acc, val) => {
|
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
messages={data ? data.messages : []}
|
||||||
}, 0);
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
|
/>
|
||||||
const handleMarkConversationAsRead = async () => {
|
);
|
||||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
|
||||||
setMarkingAsReadInProgress(true);
|
|
||||||
await markConversationRead({});
|
|
||||||
await axios.post("/sms/markConversationRead", {
|
|
||||||
conversationid: selectedConversation,
|
|
||||||
imexshopid: bodyshop.imexshopid,
|
|
||||||
});
|
|
||||||
setMarkingAsReadInProgress(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChatConversationComponent
|
|
||||||
subState={[loading || convoLoading, error || convoError]}
|
|
||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
|
||||||
messages={data ? data.messages : []}
|
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,59 @@
|
|||||||
import {PlusOutlined} from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import {useMutation} from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {Input, notification, Spin, Tag, Tooltip} from "antd";
|
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {UPDATE_CONVERSATION_LABEL} from "../../graphql/conversations.queries";
|
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||||
|
|
||||||
export default function ChatLabel({conversation}) {
|
export default function ChatLabel({ conversation }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [value, setValue] = useState(conversation.label);
|
const [value, setValue] = useState(conversation.label);
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await updateLabel({
|
const response = await updateLabel({
|
||||||
variables: {id: conversation.id, label: value},
|
variables: { id: conversation.id, label: value }
|
||||||
});
|
});
|
||||||
if (response.errors) {
|
if (response.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("messages.errors.updatinglabel", {
|
message: t("messages.errors.updatinglabel", {
|
||||||
error: JSON.stringify(response.errors),
|
error: JSON.stringify(response.errors)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("messages.errors.updatinglabel", {
|
message: t("messages.errors.updatinglabel", {
|
||||||
error: JSON.stringify(error),
|
error: JSON.stringify(error)
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
|
||||||
};
|
|
||||||
if (editing) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
onBlur={handleSave}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
{loading && <Spin size="small"/>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return conversation.label && conversation.label.trim() !== "" ? (
|
|
||||||
<Tag style={{cursor: "pointer"}} onClick={() => setEditing(true)}>
|
|
||||||
{conversation.label}
|
|
||||||
</Tag>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={t("messaging.labels.addlabel")}>
|
|
||||||
<PlusOutlined
|
|
||||||
style={{cursor: "pointer"}}
|
|
||||||
onClick={() => setEditing(true)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if (editing) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input autoFocus value={value} onChange={(e) => setValue(e.target.value)} onBlur={handleSave} allowClear />
|
||||||
|
{loading && <Spin size="small" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return conversation.label && conversation.label.trim() !== "" ? (
|
||||||
|
<Tag style={{ cursor: "pointer" }} onClick={() => setEditing(true)}>
|
||||||
|
{conversation.label}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={t("messaging.labels.addlabel")}>
|
||||||
|
<PlusOutlined style={{ cursor: "pointer" }} onClick={() => setEditing(true)} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,82 @@
|
|||||||
import {PictureFilled} from "@ant-design/icons";
|
import { PictureFilled } from "@ant-design/icons";
|
||||||
import {useQuery} from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import {Badge, Popover} from "antd";
|
import { Badge, Popover } from "antd";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries";
|
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||||
import JobDocumentsLocalGalleryExternal
|
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||||
|
|
||||||
export function ChatMediaSelector({
|
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
selectedMedia,
|
const [open, setOpen] = useState(false);
|
||||||
setSelectedMedia,
|
|
||||||
conversation,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const {loading, error, data} = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId:
|
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
||||||
conversation.job_conversations[0] &&
|
},
|
||||||
conversation.job_conversations[0].jobid,
|
|
||||||
},
|
|
||||||
|
|
||||||
skip:
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
!open ||
|
});
|
||||||
!conversation.job_conversations ||
|
|
||||||
conversation.job_conversations.length === 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleVisibleChange = (change) => {
|
const handleVisibleChange = (change) => {
|
||||||
setOpen(change);
|
setOpen(change);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedMedia([]);
|
setSelectedMedia([]);
|
||||||
}, [setSelectedMedia, conversation]);
|
}, [setSelectedMedia, conversation]);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div>
|
||||||
{loading && <LoadingSpinner/>}
|
{loading && <LoadingSpinner />}
|
||||||
{error && <AlertComponent message={error.message} type="error"/>}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{color: "red"}}>{t("messaging.labels.maxtenimages")}</div>
|
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!bodyshop.uselocalmediaserver && data && (
|
{!bodyshop.uselocalmediaserver && data && (
|
||||||
<JobDocumentsGalleryExternal
|
<JobDocumentsGalleryExternal
|
||||||
data={data ? data.documents : []}
|
data={data ? data.documents : []}
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||||
conversation.job_conversations[0] &&
|
/>
|
||||||
conversation.job_conversations[0].jobid
|
)}
|
||||||
}
|
</div>
|
||||||
/>
|
);
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
conversation.job_conversations.length === 0 ? (
|
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
|
||||||
<div>{t("messaging.errors.noattachedjobs")}</div>
|
}
|
||||||
) : (
|
title={t("messaging.labels.selectmedia")}
|
||||||
content
|
trigger="click"
|
||||||
)
|
open={open}
|
||||||
}
|
onOpenChange={handleVisibleChange}
|
||||||
title={t("messaging.labels.selectmedia")}
|
>
|
||||||
trigger="click"
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
open={open}
|
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||||
onOpenChange={handleVisibleChange}
|
</Badge>
|
||||||
>
|
</Popover>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
);
|
||||||
<PictureFilled style={{margin: "0 .5rem"}}/>
|
|
||||||
</Badge>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +1,106 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import {Tooltip} from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, {useEffect, useRef} from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import {MdDone, MdDoneAll} from "react-icons/md";
|
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized";
|
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
|
||||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({messages}) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
const virtualizedListRef = useRef(null);
|
const virtualizedListRef = useRef(null);
|
||||||
|
|
||||||
const _cache = new CellMeasurerCache({
|
const _cache = new CellMeasurerCache({
|
||||||
fixedWidth: true,
|
fixedWidth: true,
|
||||||
// minHeight: 50,
|
// minHeight: 50,
|
||||||
defaultHeight: 100,
|
defaultHeight: 100
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollToBottom = (renderedrows) => {
|
const scrollToBottom = (renderedrows) => {
|
||||||
//console.log("Scrolling to", messages.length);
|
//console.log("Scrolling to", messages.length);
|
||||||
// !!virtualizedListRef.current &&
|
// !!virtualizedListRef.current &&
|
||||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
// virtualizedListRef.current.scrollToRow(messages.length);
|
||||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
||||||
//Scrolling does not work on this version of React.
|
//Scrolling does not work on this version of React.
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(scrollToBottom, [messages]);
|
useEffect(scrollToBottom, [messages]);
|
||||||
|
|
||||||
const _rowRenderer = ({index, key, parent, style}) => {
|
|
||||||
return (
|
|
||||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
|
||||||
{({measure, registerChild}) => (
|
|
||||||
<div
|
|
||||||
ref={registerChild}
|
|
||||||
onLoad={measure}
|
|
||||||
style={style}
|
|
||||||
className={`${
|
|
||||||
messages[index].isoutbound ? "mine messages" : "yours messages"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="message msgmargin">
|
|
||||||
{MessageRender(messages[index])}
|
|
||||||
{StatusRender(messages[index].status)}
|
|
||||||
</div>
|
|
||||||
{messages[index].isoutbound && (
|
|
||||||
<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"
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const _rowRenderer = ({ index, key, parent, style }) => {
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
||||||
<AutoSizer>
|
{({ measure, registerChild }) => (
|
||||||
{({height, width}) => (
|
<div
|
||||||
<List
|
ref={registerChild}
|
||||||
ref={virtualizedListRef}
|
onLoad={measure}
|
||||||
width={width}
|
style={style}
|
||||||
height={height}
|
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
||||||
rowHeight={_cache.rowHeight}
|
>
|
||||||
rowRenderer={_rowRenderer}
|
<div className="message msgmargin">
|
||||||
rowCount={messages.length}
|
{MessageRender(messages[index])}
|
||||||
overscanRowCount={10}
|
{StatusRender(messages[index].status)}
|
||||||
estimatedRowSize={150}
|
</div>
|
||||||
scrollToIndex={messages.length}
|
{messages[index].isoutbound && (
|
||||||
/>
|
<div style={{ fontSize: 10 }}>
|
||||||
)}
|
{i18n.t("messaging.labels.sentby", {
|
||||||
</AutoSizer>
|
by: messages[index].userid,
|
||||||
</div>
|
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CellMeasurer>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat">
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List
|
||||||
|
ref={virtualizedListRef}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
rowHeight={_cache.rowHeight}
|
||||||
|
rowRenderer={_rowRenderer}
|
||||||
|
rowCount={messages.length}
|
||||||
|
overscanRowCount={10}
|
||||||
|
estimatedRowSize={150}
|
||||||
|
scrollToIndex={messages.length}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageRender = (message) => {
|
const MessageRender = (message) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={DateTimeFormatter({children: message.created_at})}>
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
<div>
|
<div>
|
||||||
{message.image_path &&
|
{message.image_path &&
|
||||||
message.image_path.map((i, idx) => (
|
message.image_path.map((i, idx) => (
|
||||||
<div
|
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||||
key={idx}
|
<a href={i} target="__blank">
|
||||||
style={{display: "flex", justifyContent: "center"}}
|
<img alt="Received" className="message-img" src={i} />
|
||||||
>
|
</a>
|
||||||
<a href={i} target="__blank">
|
|
||||||
<img alt="Received" className="message-img" src={i}/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div>{message.text}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
))}
|
||||||
);
|
<div>{message.text}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusRender = (status) => {
|
const StatusRender = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "sent":
|
case "sent":
|
||||||
return <Icon component={MdDone} className="message-icon"/>;
|
return <Icon component={MdDone} className="message-icon" />;
|
||||||
case "delivered":
|
case "delivered":
|
||||||
return <Icon component={MdDoneAll} className="message-icon"/>;
|
return <Icon component={MdDoneAll} className="message-icon" />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,55 +1,49 @@
|
|||||||
import {PlusCircleFilled} from "@ant-design/icons";
|
import { PlusCircleFilled } from "@ant-design/icons";
|
||||||
import {Button, Form, Popover} from "antd";
|
import { Button, Form, Popover } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {openChatByPhone} from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatNewConversation({openChatByPhone}) {
|
export function ChatNewConversation({ openChatByPhone }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
openChatByPhone({phone_num: values.phoneNumber});
|
openChatByPhone({ phone_num: values.phoneNumber });
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Form form={form} onFinish={handleFinish}>
|
<Form form={form} onFinish={handleFinish}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("messaging.labels.phonenumber")}
|
label={t("messaging.labels.phonenumber")}
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
rules={[
|
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "phoneNumber")]}
|
||||||
({getFieldValue}) =>
|
>
|
||||||
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
|
<PhoneFormItem />
|
||||||
]}
|
</Form.Item>
|
||||||
>
|
<Button type="primary" htmlType="submit">
|
||||||
<PhoneFormItem/>
|
{t("messaging.actions.new")}
|
||||||
</Form.Item>
|
</Button>
|
||||||
<Button type="primary" htmlType="submit">
|
</Form>
|
||||||
{t("messaging.actions.new")}
|
</div>
|
||||||
</Button>
|
);
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover trigger="click" content={popContent}>
|
<Popover trigger="click" content={popContent}>
|
||||||
<PlusCircleFilled/>
|
<PlusCircleFilled />
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatNewConversation);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ChatNewConversation);
|
|
||||||
|
|||||||
@@ -1,54 +1,47 @@
|
|||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {openChatByPhone} from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {searchingForConversation} from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
searchingForConversation: searchingForConversation,
|
searchingForConversation: searchingForConversation
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatOpenButton({
|
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||||
bodyshop,
|
const { t } = useTranslation();
|
||||||
searchingForConversation,
|
if (!phone) return <></>;
|
||||||
phone,
|
|
||||||
jobid,
|
|
||||||
openChatByPhone,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
if (!phone) return <></>;
|
|
||||||
|
|
||||||
if (!bodyshop.messagingservicesid)
|
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||||
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="# "
|
href="# "
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({phone_num: p.formatInternational(), jobid: jobid});
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({message: t("messaging.error.invalidphone")});
|
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons";
|
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import {useLazyQuery, useQuery} from "@apollo/client";
|
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import {Badge, Card, Col, Row, Space, Tag, Tooltip, Typography} from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, {useCallback, useEffect, useState} from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries";
|
import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
|
||||||
import {toggleChatVisible} from "../../redux/messaging/messaging.actions";
|
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||||
import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors";
|
import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||||
@@ -15,119 +15,102 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
|||||||
import "./chat-popup.styles.scss";
|
import "./chat-popup.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
chatVisible: selectChatVisible,
|
chatVisible: selectChatVisible
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPopupComponent({
|
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||||
chatVisible,
|
const { t } = useTranslation();
|
||||||
selectedConversation,
|
const [pollInterval, setpollInterval] = useState(0);
|
||||||
toggleChatVisible,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
|
||||||
|
|
||||||
const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, {
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
...(pollInterval > 0 ? {pollInterval} : {}),
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
const [getConversations, {loading, data, refetch, fetchMore}] =
|
const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||||
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
fetchPolicy: "network-only",
|
||||||
fetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
skip: !chatVisible,
|
||||||
skip: !chatVisible,
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
...(pollInterval > 0 ? {pollInterval} : {}),
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fcmToken) {
|
if (fcmToken) {
|
||||||
setpollInterval(0);
|
setpollInterval(0);
|
||||||
} else {
|
} else {
|
||||||
setpollInterval(60000);
|
setpollInterval(60000);
|
||||||
|
}
|
||||||
|
}, [fcmToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatVisible)
|
||||||
|
getConversations({
|
||||||
|
variables: {
|
||||||
|
offset: 0
|
||||||
}
|
}
|
||||||
}, [fcmToken]);
|
});
|
||||||
|
}, [chatVisible, getConversations]);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadMoreConversations = useCallback(() => {
|
||||||
if (chatVisible)
|
if (data)
|
||||||
getConversations({
|
fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
offset: 0,
|
offset: data.conversations.length
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}, [chatVisible, getConversations]);
|
}, [data, fetchMore]);
|
||||||
|
|
||||||
const loadMoreConversations = useCallback(() => {
|
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||||
if (data)
|
|
||||||
fetchMore({
|
|
||||||
variables: {
|
|
||||||
offset: data.conversations.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [data, fetchMore]);
|
|
||||||
|
|
||||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
return (
|
||||||
|
<Badge count={unreadCount}>
|
||||||
|
<Card size="small">
|
||||||
|
{chatVisible ? (
|
||||||
|
<div className="chat-popup">
|
||||||
|
<Space align="center">
|
||||||
|
<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>}
|
||||||
|
</Space>
|
||||||
|
<ShrinkOutlined
|
||||||
|
onClick={() => toggleChatVisible()}
|
||||||
|
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||||
<Badge count={unreadCount}>
|
<Col span={8}>
|
||||||
<Card size="small">
|
{loading ? (
|
||||||
{chatVisible ? (
|
<LoadingSpinner />
|
||||||
<div className="chat-popup">
|
|
||||||
<Space align="center">
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
<ShrinkOutlined
|
|
||||||
onClick={() => toggleChatVisible()}
|
|
||||||
style={{position: "absolute", right: ".5rem", top: ".5rem"}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
|
||||||
<Col span={8}>
|
|
||||||
{loading ? (
|
|
||||||
<LoadingSpinner/>
|
|
||||||
) : (
|
|
||||||
<ChatConversationListComponent
|
|
||||||
conversationList={data ? data.conversations : []}
|
|
||||||
loadMoreConversations={loadMoreConversations}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
<Col span={16}>
|
|
||||||
{selectedConversation ? <ChatConversationContainer/> : null}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<ChatConversationListComponent
|
||||||
onClick={() => toggleChatVisible()}
|
conversationList={data ? data.conversations : []}
|
||||||
style={{cursor: "pointer"}}
|
loadMoreConversations={loadMoreConversations}
|
||||||
>
|
/>
|
||||||
<MessageOutlined className="chat-popup-info-icon"/>
|
|
||||||
<strong>{t("messaging.labels.messaging")}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Col>
|
||||||
</Badge>
|
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
||||||
);
|
</Row>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div onClick={() => toggleChatVisible()} style={{ cursor: "pointer" }}>
|
||||||
|
<MessageOutlined className="chat-popup-info-icon" />
|
||||||
|
<strong>{t("messaging.labels.messaging")}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
|
||||||
|
|||||||
@@ -1,38 +1,34 @@
|
|||||||
import {PlusCircleOutlined} from "@ant-design/icons";
|
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||||
import {Dropdown} from "antd";
|
import { Dropdown } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {setMessage} from "../../redux/messaging/messaging.actions";
|
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
setMessage: (message) => dispatch(setMessage(message)),
|
setMessage: (message) => dispatch(setMessage(message))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPresetsComponent({bodyshop, setMessage, className}) {
|
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||||
|
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||||
|
key: idx,
|
||||||
|
label: i.label,
|
||||||
|
onClick: () => setMessage(i.text)
|
||||||
|
}));
|
||||||
|
|
||||||
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
return (
|
||||||
key: idx,
|
<div className={className}>
|
||||||
label: (i.label),
|
<Dropdown trigger={["click"]} menu={{ items }}>
|
||||||
onClick: () => setMessage(i.text),
|
<PlusCircleOutlined />
|
||||||
}));
|
</Dropdown>
|
||||||
|
</div>
|
||||||
return (
|
);
|
||||||
<div className={className}>
|
|
||||||
<Dropdown trigger={["click"]} menu={{items}}>
|
|
||||||
<PlusCircleOutlined/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatPresetsComponent);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ChatPresetsComponent);
|
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import {MailOutlined, PrinterOutlined} from "@ant-design/icons";
|
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
||||||
import {Space, Spin} from "antd";
|
import { Space, Spin } from "antd";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {setEmailOptions} from "../../redux/email/email.actions";
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import {TemplateList} from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
setEmailOptions: (e) => dispatch(setEmailOptions(e))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPrintButton({conversation}) {
|
export function ChatPrintButton({ conversation }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const generateDocument = (type) => {
|
const generateDocument = (type) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
GenerateDocument(
|
GenerateDocument(
|
||||||
{
|
{
|
||||||
name: TemplateList("messaging").conversation_list.key,
|
name: TemplateList("messaging").conversation_list.key,
|
||||||
variables: {id: conversation.id},
|
variables: { id: conversation.id }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: TemplateList("messaging").conversation_list.subject,
|
subject: TemplateList("messaging").conversation_list.subject
|
||||||
},
|
},
|
||||||
type,
|
type,
|
||||||
conversation.id
|
conversation.id
|
||||||
).catch(e => {
|
).catch((e) => {
|
||||||
console.warn('Something went wrong generating a document.');
|
console.warn("Something went wrong generating a document.");
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PrinterOutlined onClick={() => generateDocument('p')}/>
|
<PrinterOutlined onClick={() => generateDocument("p")} />
|
||||||
<MailOutlined onClick={() => generateDocument('e')}/>
|
<MailOutlined onClick={() => generateDocument("e")} />
|
||||||
{loading && <Spin/>}
|
{loading && <Spin />}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user