- Merge client update into test-beta
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -183,6 +183,26 @@ jobs:
|
|||||||
- jira/notify
|
- jira/notify
|
||||||
|
|
||||||
|
|
||||||
|
app-beta-build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:18.18.2
|
||||||
|
resource_class: large
|
||||||
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
path: ~/repo
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: npm i
|
||||||
|
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
- aws-s3/sync:
|
||||||
|
from: build
|
||||||
|
to: "s3://imex-online-beta/"
|
||||||
|
- jira/notify
|
||||||
|
|
||||||
test-hasura-migrate:
|
test-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.15.0
|
- image: cimg/node:16.15.0
|
||||||
@@ -233,6 +253,27 @@ jobs:
|
|||||||
to: "s3://imex-online-test/"
|
to: "s3://imex-online-test/"
|
||||||
- jira/notify
|
- jira/notify
|
||||||
|
|
||||||
|
test-app-beta-build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:18.18.2
|
||||||
|
resource_class: large
|
||||||
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
path: ~/repo
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: npm i
|
||||||
|
|
||||||
|
- run: npm run build:test
|
||||||
|
|
||||||
|
- aws-s3/sync:
|
||||||
|
from: build
|
||||||
|
to: "s3://imex-online-test-beta/"
|
||||||
|
- jira/notify
|
||||||
|
|
||||||
admin-app-build:
|
admin-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.15.0
|
- image: cimg/node:16.15.0
|
||||||
@@ -274,6 +315,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master
|
only: master
|
||||||
|
- app-beta-build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master-beta
|
||||||
- hasura-migrate:
|
- hasura-migrate:
|
||||||
secret: ${HASURA_PROD_SECRET}
|
secret: ${HASURA_PROD_SECRET}
|
||||||
filters:
|
filters:
|
||||||
@@ -296,6 +341,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: test
|
only: test
|
||||||
|
- test-app-beta-build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: test-beta
|
||||||
- test-hasura-migrate:
|
- test-hasura-migrate:
|
||||||
secret: ${HASURA_TEST_SECRET}
|
secret: ${HASURA_TEST_SECRET}
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
1
client/.npmrc
Normal file
1
client/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
legacy-peer-deps=true
|
||||||
@@ -2,72 +2,89 @@
|
|||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||||
|
const {convertLegacyToken} = require('@ant-design/compatible/lib');
|
||||||
|
const {theme} = require('antd/lib');
|
||||||
|
|
||||||
|
const {defaultAlgorithm, defaultSeed} = theme;
|
||||||
|
|
||||||
|
const mapToken = defaultAlgorithm(defaultSeed);
|
||||||
|
const v4Token = convertLegacyToken(mapToken);
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
plugin: SentryWebpackPlugin,
|
plugin: SentryWebpackPlugin,
|
||||||
options: {
|
options: {
|
||||||
// sentry-cli configuration
|
// sentry-cli configuration
|
||||||
authToken:
|
authToken:
|
||||||
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
||||||
org: "snapt-software",
|
org: "snapt-software",
|
||||||
project: "rome-online",
|
project: "rome-online",
|
||||||
release: process.env.REACT_APP_GIT_SHA,
|
release: process.env.REACT_APP_GIT_SHA,
|
||||||
|
|
||||||
// webpack-specific configuration
|
// webpack-specific configuration
|
||||||
include: ".",
|
include: ".",
|
||||||
ignore: ["node_modules", "webpack.config.js"],
|
ignore: ["node_modules", "webpack.config.js"],
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plugin: CracoLessPlugin,
|
|
||||||
options: {
|
|
||||||
lessLoaderOptions: {
|
|
||||||
lessOptions: {
|
|
||||||
modifyVars: {
|
|
||||||
...(process.env.NODE_ENV === "development"
|
|
||||||
? { "@primary-color": "#B22234" }
|
|
||||||
: {
|
|
||||||
//"@primary-color": "#1DA57A"
|
|
||||||
}),
|
|
||||||
// "@primary-color": " #1890ff", // primary color for all components
|
|
||||||
// "@link-color": "#1890ff", // link color
|
|
||||||
// "@success-color": "#52c41a", // success state color
|
|
||||||
// "@warning-color": "#faad14", // warning state color
|
|
||||||
// "@error-color": "#f5222d", // error state color
|
|
||||||
// "@font-size-base": "14px", // major text font size
|
|
||||||
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
|
|
||||||
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
|
|
||||||
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
|
|
||||||
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
|
|
||||||
// "@border-radius-base": "2px", // major border radius
|
|
||||||
// "@border-color-base": "#d9d9d9", // major border color
|
|
||||||
// "@box-shadow-base":
|
|
||||||
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
|
|
||||||
},
|
},
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
},
|
plugin: CracoLessPlugin,
|
||||||
],
|
options: {
|
||||||
webpack: {
|
lessLoaderOptions: {
|
||||||
configure: (webpackConfig) => ({
|
lessOptions: {
|
||||||
...webpackConfig,
|
modifyVars: {
|
||||||
optimization: {
|
...v4Token,
|
||||||
...webpackConfig.optimization,
|
// TODO: This will no longer work in AntD 5.0
|
||||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
...(process.env.NODE_ENV === "development"
|
||||||
// https://github.com/facebook/create-react-app/issues/8320
|
? {"colorPrimary": "#B22234"}
|
||||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
: {
|
||||||
if (item instanceof TerserPlugin) {
|
//"@primary-color": "#1DA57A"
|
||||||
item.options.parallel = 2;
|
}),
|
||||||
}
|
// "@primary-color": " #1890ff", // primary color for all components
|
||||||
|
// "@link-color": "#1890ff", // link color
|
||||||
|
// "@success-color": "#52c41a", // success state color
|
||||||
|
// "@warning-color": "#faad14", // warning state color
|
||||||
|
// "@error-color": "#f5222d", // error state color
|
||||||
|
// "@font-size-base": "14px", // major text font size
|
||||||
|
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
|
||||||
|
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
|
||||||
|
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
|
||||||
|
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
|
||||||
|
// "@border-radius-base": "2px", // major border radius
|
||||||
|
// "@border-color-base": "#d9d9d9", // major border color
|
||||||
|
// "@box-shadow-base":
|
||||||
|
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webpack: {
|
||||||
|
configure: (webpackConfig) => {
|
||||||
|
return {
|
||||||
|
...webpackConfig,
|
||||||
|
// Required for Dev Server
|
||||||
|
devServer: {
|
||||||
|
...webpackConfig.devServer,
|
||||||
|
allowedHosts: 'all',
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
...webpackConfig.optimization,
|
||||||
|
// Workaround for CircleCI bug caused by the number of CPUs shown
|
||||||
|
// https://github.com/facebook/create-react-app/issues/8320
|
||||||
|
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
||||||
|
if (item instanceof TerserPlugin) {
|
||||||
|
item.options.parallel = 2;
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
},
|
},
|
||||||
devtool: "source-map",
|
},
|
||||||
|
devtool: "source-map",
|
||||||
};
|
};
|
||||||
|
|||||||
17
client/cypress.config.js
Normal file
17
client/cypress.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { defineConfig } = require('cypress')
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
experimentalStudio: true,
|
||||||
|
env: {
|
||||||
|
FIREBASE_USERNAME: 'cypress@imex.test',
|
||||||
|
FIREBASE_PASSWORD: 'cypress',
|
||||||
|
},
|
||||||
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"experimentalStudio": true,
|
|
||||||
"env": {
|
|
||||||
"FIREBASE_USERNAME": "cypress@imex.test",
|
|
||||||
"FIREBASE_PASSWORD": "cypress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8872,13 +8872,13 @@
|
|||||||
│ ├─ email: luis@luisrudge.net
|
│ ├─ email: luis@luisrudge.net
|
||||||
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
|
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
|
||||||
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
|
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
|
||||||
├─ postcss-focus-visible@4.0.0
|
├─ postcss-focus-open@4.0.0
|
||||||
│ ├─ licenses: CC0-1.0
|
│ ├─ licenses: CC0-1.0
|
||||||
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
|
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
|
||||||
│ ├─ publisher: Jonathan Neal
|
│ ├─ publisher: Jonathan Neal
|
||||||
│ ├─ email: jonathantneal@hotmail.com
|
│ ├─ email: jonathantneal@hotmail.com
|
||||||
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
|
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
|
||||||
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
|
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
|
||||||
├─ postcss-focus-within@3.0.0
|
├─ postcss-focus-within@3.0.0
|
||||||
│ ├─ licenses: CC0-1.0
|
│ ├─ licenses: CC0-1.0
|
||||||
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within
|
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within
|
||||||
|
|||||||
15967
client/package-lock.json
generated
15967
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,85 +4,83 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.9",
|
"@ant-design/compatible": "^5.1.2",
|
||||||
|
"@ant-design/pro-layout": "^7.17.16",
|
||||||
|
"@apollo/client": "^3.8.10",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@asseinfo/react-kanban": "^2.2.0",
|
||||||
"@craco/craco": "^7.0.0",
|
"@craco/craco": "^7.1.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
"@fingerprintjs/fingerprintjs": "^4.2.1",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@sentry/react": "^7.40.0",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@sentry/tracing": "^7.40.0",
|
"@sentry/react": "^7.93.0",
|
||||||
"@splitsoftware/splitio-react": "^1.8.1",
|
"@sentry/tracing": "^7.93.0",
|
||||||
"@tanem/react-nprogress": "^5.0.8",
|
"@splitsoftware/splitio-react": "^1.11.0",
|
||||||
"antd": "^4.24.8",
|
"@tanem/react-nprogress": "^5.0.51",
|
||||||
|
"antd": "^5.12.8",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.6.5",
|
||||||
"craco-less": "^2.0.0",
|
"craco-less": "^3.0.1",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"dayjs-business-days2": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.3.1",
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^9.17.1",
|
"firebase": "^10.7.2",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^23.7.16",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
"jsoneditor": "^9.9.0",
|
"jsoneditor": "^10.0.0",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.10.21",
|
"libphonenumber-js": "^1.10.53",
|
||||||
"logrocket": "^3.0.1",
|
"logrocket": "^7.0.0",
|
||||||
"markerjs2": "^2.28.1",
|
"markerjs2": "^2.31.4",
|
||||||
"moment-business-days": "^1.2.0",
|
|
||||||
"moment-timezone": "^0.5.41",
|
|
||||||
"normalize-url": "^8.0.0",
|
"normalize-url": "^8.0.0",
|
||||||
"phone": "^3.1.35",
|
"phone": "^3.1.42",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^8.1.0",
|
||||||
"rc-queue-anim": "^2.0.0",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-big-calendar": "^1.6.8",
|
"react-big-calendar": "^1.8.6",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^7.0.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-drag-listview": "^0.2.1",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.0",
|
"react-grid-gallery": "^1.0.0",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^12.2.0",
|
"react-i18next": "^14.0.0",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^5.0.1",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-intersection-observer": "^9.4.3",
|
"react-intersection-observer": "^9.5.3",
|
||||||
"react-number-format": "^5.1.3",
|
"react-number-format": "^5.1.4",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^9.1.0",
|
||||||
"react-resizable": "^3.0.4",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^6.21.3",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.5",
|
||||||
"recharts": "^2.4.3",
|
"recharts": "^2.10.4",
|
||||||
"redux": "^4.2.1",
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.2.2",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^4.1.7",
|
"reselect": "^5.1.0",
|
||||||
"sass": "^1.58.3",
|
"sass": "^1.70.0",
|
||||||
"socket.io-client": "^4.6.1",
|
"socket.io-client": "^4.7.4",
|
||||||
"styled-components": "^5.3.6",
|
"styled-components": "^6.1.8",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"web-vitals": "^2.1.4",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"workbox-background-sync": "^6.5.3",
|
"web-vitals": "^3.5.1",
|
||||||
"workbox-broadcast-update": "^6.5.3",
|
"workbox-core": "^7.0.0",
|
||||||
"workbox-cacheable-response": "^6.5.3",
|
"workbox-expiration": "^7.0.0",
|
||||||
"workbox-core": "^6.5.3",
|
"workbox-navigation-preload": "^7.0.0",
|
||||||
"workbox-expiration": "^6.5.3",
|
"workbox-precaching": "^7.0.0",
|
||||||
"workbox-google-analytics": "^6.5.3",
|
"workbox-routing": "^7.0.0",
|
||||||
"workbox-navigation-preload": "^6.5.3",
|
"workbox-strategies": "^7.0.0",
|
||||||
"workbox-precaching": "^6.5.3",
|
|
||||||
"workbox-range-requests": "^6.5.3",
|
|
||||||
"workbox-routing": "^6.5.3",
|
|
||||||
"workbox-strategies": "^6.5.3",
|
|
||||||
"workbox-streams": "^6.5.3",
|
|
||||||
"yauzl": "^2.10.0"
|
"yauzl": "^2.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -119,12 +117,13 @@
|
|||||||
"react-error-overlay": "6.0.9"
|
"react-error-overlay": "6.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/webpack-plugin": "^1.20.0",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@testing-library/cypress": "^8.0.3",
|
"@sentry/webpack-plugin": "^2.10.2",
|
||||||
"cypress": "^10.3.1",
|
"@testing-library/cypress": "^10.0.1",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"cypress": "^13.6.3",
|
||||||
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ This package contains the following license and notice below:
|
|||||||
# @firebase/logger
|
# @firebase/logger
|
||||||
|
|
||||||
This package serves as the base of all logging in the JS SDK. Any logging that
|
This package serves as the base of all logging in the JS SDK. Any logging that
|
||||||
is intended to be visible to Firebase end developers should go through this
|
is intended to be open to Firebase end developers should go through this
|
||||||
module.
|
module.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
@@ -9375,7 +9375,7 @@ parties to make or receive copies. Mere interaction with a user through
|
|||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently open
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ The following NPM packages may be included in this product:
|
|||||||
- postcss-dir-pseudo-class@5.0.0
|
- postcss-dir-pseudo-class@5.0.0
|
||||||
- postcss-double-position-gradients@1.0.0
|
- postcss-double-position-gradients@1.0.0
|
||||||
- postcss-env-function@2.0.2
|
- postcss-env-function@2.0.2
|
||||||
- postcss-focus-visible@4.0.0
|
- postcss-focus-open@4.0.0
|
||||||
- postcss-focus-within@3.0.0
|
- postcss-focus-within@3.0.0
|
||||||
- postcss-gap-properties@2.0.0
|
- postcss-gap-properties@2.0.0
|
||||||
- postcss-image-set-function@3.0.1
|
- postcss-image-set-function@3.0.1
|
||||||
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
|
|||||||
# @firebase/logger
|
# @firebase/logger
|
||||||
|
|
||||||
This package serves as the base of all logging in the JS SDK. Any logging that
|
This package serves as the base of all logging in the JS SDK. Any logging that
|
||||||
is intended to be visible to Firebase end developers should go through this
|
is intended to be open to Firebase end developers should go through this
|
||||||
module.
|
module.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
@@ -24029,7 +24029,7 @@ parties to make or receive copies. Mere interaction with a user through
|
|||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently open
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { ApolloProvider } from "@apollo/client";
|
|||||||
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
|
import { SplitFactory, 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 moment from "moment";
|
import dayjs from "../utils/day";
|
||||||
|
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";
|
||||||
|
|
||||||
moment.locale("en-US");
|
dayjs.locale("en");
|
||||||
|
|
||||||
export const factory = SplitSdk({
|
export const factory = SplitSdk({
|
||||||
core: {
|
core: {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useClient } from "@splitsoftware/splitio-react";
|
import {useSplitClient} from "@splitsoftware/splitio-react";
|
||||||
import { Button, Result } from "antd";
|
import {Button, Result} from "antd";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import React, { lazy, Suspense, useEffect } from "react";
|
import React, {lazy, Suspense, 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 { Route, Switch } from "react-router-dom";
|
import {Route, Routes} from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||||
//Component Imports
|
//Component Imports
|
||||||
@@ -13,149 +13,136 @@ import LoadingSpinner from "../components/loading-spinner/loading-spinner.compon
|
|||||||
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||||
import LandingPage from "../pages/landing/landing.page";
|
import LandingPage from "../pages/landing/landing.page";
|
||||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
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, selectCurrentUser,} from "../redux/user/user.selectors";
|
||||||
selectBodyshop,
|
import PrivateRoute from "../components/PrivateRoute";
|
||||||
selectCurrentUser,
|
|
||||||
} from "../redux/user/user.selectors";
|
|
||||||
import PrivateRoute from "../utils/private-route";
|
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
|
import handleBeta from "../utils/betaHandler";
|
||||||
|
|
||||||
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,
|
||||||
});
|
});
|
||||||
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}) {
|
||||||
bodyshop,
|
|
||||||
checkUserSession,
|
|
||||||
currentUser,
|
|
||||||
online,
|
|
||||||
setOnline,
|
|
||||||
}) {
|
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const client = useSplitClient().client;
|
||||||
if (!navigator.onLine) {
|
const [listenersAdded, setListenersAdded] = useState(false)
|
||||||
setOnline(false);
|
const {t} = useTranslation();
|
||||||
}
|
|
||||||
|
|
||||||
checkUserSession();
|
|
||||||
}, [checkUserSession, setOnline]);
|
|
||||||
|
|
||||||
//const b = Grid.useBreakpoint();
|
useEffect(() => {
|
||||||
// console.log("Breakpoints:", b);
|
if (!navigator.onLine) {
|
||||||
|
setOnline(false);
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
window.addEventListener("offline", function (e) {
|
|
||||||
setOnline(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("online", function (e) {
|
|
||||||
setOnline(true);
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentUser.authorized && bodyshop) {
|
|
||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
|
||||||
|
|
||||||
LogRocket.init("rome-online/rome-online");
|
|
||||||
if (client.getTreatment("LogRocket_Tracking") === "on") {
|
|
||||||
LogRocket.init("rome-online/rome-online");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [bodyshop, client, currentUser.authorized]);
|
|
||||||
|
|
||||||
if (currentUser.authorized === null) {
|
|
||||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!online)
|
|
||||||
return (
|
|
||||||
<Result
|
|
||||||
status="warning"
|
|
||||||
title={t("general.labels.nointernet")}
|
|
||||||
subTitle={t("general.labels.nointernet_sub")}
|
|
||||||
extra={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.refresh")}
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
checkUserSession();
|
||||||
<Switch>
|
}, [checkUserSession, setOnline]);
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
|
||||||
<ErrorBoundary>
|
//const b = Grid.useBreakpoint();
|
||||||
<Route exact path="/" component={LandingPage} />
|
// console.log("Breakpoints:", b);
|
||||||
</ErrorBoundary>
|
|
||||||
<ErrorBoundary>
|
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||||
<Route exact path="/signin" component={SignInPage} />
|
useEffect(() => {
|
||||||
</ErrorBoundary>
|
const offlineListener = (e) => {
|
||||||
<ErrorBoundary>
|
setOnline(false);
|
||||||
<Route exact path="/resetpassword" component={ResetPassword} />
|
}
|
||||||
</ErrorBoundary>
|
|
||||||
<ErrorBoundary>
|
const onlineListener = (e) => {
|
||||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
setOnline(true);
|
||||||
</ErrorBoundary>
|
}
|
||||||
<ErrorBoundary>
|
|
||||||
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
if (!listenersAdded) {
|
||||||
</ErrorBoundary>
|
console.log('Added events for offline and online');
|
||||||
<ErrorBoundary>
|
window.addEventListener("offline", offlineListener);
|
||||||
<Route
|
window.addEventListener("online", onlineListener);
|
||||||
exact
|
setListenersAdded(true);
|
||||||
path="/mp/:paymentIs"
|
}
|
||||||
component={MobilePaymentContainer}
|
|
||||||
/>
|
return () => {
|
||||||
</ErrorBoundary>
|
window.removeEventListener("offline", offlineListener);
|
||||||
<ErrorBoundary>
|
window.removeEventListener("online", onlineListener);
|
||||||
<PrivateRoute
|
}
|
||||||
isAuthorized={currentUser.authorized}
|
}, [setOnline, listenersAdded]);
|
||||||
path="/manage"
|
|
||||||
component={ManagePage}
|
useEffect(() => {
|
||||||
/>
|
if (currentUser.authorized && bodyshop) {
|
||||||
</ErrorBoundary>
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
<ErrorBoundary>
|
|
||||||
<PrivateRoute
|
if (
|
||||||
isAuthorized={currentUser.authorized}
|
client.getTreatment("LogRocket_Tracking") === "on" ||
|
||||||
path="/tech"
|
window.location.hostname === 'beta.romeonline.io'
|
||||||
component={TechPageContainer}
|
) {
|
||||||
/>
|
console.log("LR Start");
|
||||||
</ErrorBoundary>
|
LogRocket.init("rome-online/rome-online");
|
||||||
<ErrorBoundary>
|
}
|
||||||
<PrivateRoute
|
}
|
||||||
isAuthorized={currentUser.authorized}
|
}, [bodyshop, client, currentUser.authorized]);
|
||||||
path="/edit"
|
|
||||||
component={DocumentEditorContainer}
|
if (currentUser.authorized === null) {
|
||||||
/>
|
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
|
||||||
</ErrorBoundary>
|
}
|
||||||
</Suspense>
|
|
||||||
</Switch>
|
handleBeta();
|
||||||
);
|
|
||||||
|
if (!online)
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title={t("general.labels.nointernet")}
|
||||||
|
subTitle={t("general.labels.nointernet_sub")}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.actions.refresh")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Any route that is not assigned and matched will default to the Landing Page component
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingSpinner message="Rome Online"/>}>
|
||||||
|
<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);
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
//Global Styles.
|
//Global Styles.
|
||||||
@import "react-big-calendar/lib/sass/styles";
|
@import "react-big-calendar/lib/sass/styles";
|
||||||
|
|
||||||
|
.ant-menu-item-divider {
|
||||||
|
border-bottom: 1px solid #74695c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-dark .ant-menu-item:hover {
|
||||||
|
background-color: #1890ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.imex-table-header {
|
.imex-table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
17
client/src/components/PrivateRoute.js
Normal file
17
client/src/components/PrivateRoute.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthorized) {
|
||||||
|
navigate(`/signin?redirect=${location.pathname}`);
|
||||||
|
}
|
||||||
|
}, [isAuthorized, navigate, location]);
|
||||||
|
|
||||||
|
return <Outlet/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
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,233 +1,233 @@
|
|||||||
import { Input, Table, Checkbox, Card, Space } from "antd";
|
import {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 { Link } from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
import PayableExportButton from "../payable-export-button/payable-export-button.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 { DateFormatter } from "../../utils/DateFormatter";
|
import {DateFormatter} from "../../utils/DateFormatter";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.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 ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
import {pageLimit} from "../../utils/config";
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(AccountingPayablesTableComponent);
|
)(AccountingPayablesTableComponent);
|
||||||
|
|
||||||
export function AccountingPayablesTableComponent({
|
export function AccountingPayablesTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
bills,
|
bills,
|
||||||
refetch,
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
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",
|
||||||
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{record.vendor.name}
|
<Table
|
||||||
</Link>
|
loading={loading}
|
||||||
),
|
dataSource={dataSource}
|
||||||
},
|
pagination={{position: "top", pageSize: pageLimit}}
|
||||||
{
|
columns={columns}
|
||||||
title: t("bills.fields.invoice_number"),
|
rowKey="id"
|
||||||
dataIndex: "invoice_number",
|
onChange={handleTableChange}
|
||||||
key: "invoice_number",
|
rowSelection={{
|
||||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
onSelectAll: (selected, selectedRows) =>
|
||||||
sortOrder:
|
setSelectedBills(selectedRows.map((i) => i.id)),
|
||||||
state.sortedInfo.columnKey === "invoice_number" &&
|
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||||
state.sortedInfo.order,
|
setSelectedBills(selectedRows.map((i) => i.id));
|
||||||
render: (text, record) => (
|
},
|
||||||
<Link
|
getCheckboxProps: (record) => ({
|
||||||
to={{
|
disabled: record.exported,
|
||||||
pathname: `/manage/bills`,
|
}),
|
||||||
search: queryString.stringify({
|
selectedRowKeys: selectedBills,
|
||||||
billid: record.id,
|
type: "checkbox",
|
||||||
vendorid: record.vendor.id,
|
}}
|
||||||
}),
|
/>
|
||||||
}}
|
</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",
|
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
|
||||||
|
|
||||||
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,14 +1,14 @@
|
|||||||
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 { 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 from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay 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";
|
||||||
@@ -18,215 +18,215 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
|||||||
import {pageLimit} from "../../utils/config";
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(AccountingPayablesTableComponent);
|
)(AccountingPayablesTableComponent);
|
||||||
|
|
||||||
export function AccountingPayablesTableComponent({
|
export function AccountingPayablesTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
payments,
|
payments,
|
||||||
refetch,
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
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) => (
|
render: (text, record) => (
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.date"),
|
title: t("payments.fields.date"),
|
||||||
dataIndex: "date",
|
dataIndex: "date",
|
||||||
key: "date",
|
key: "date",
|
||||||
sorter: (a, b) => dateSort(a.date, b.date),
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
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) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.job.owner ? (
|
return record.job.owner ? (
|
||||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||||
<OwnerNameDisplay ownerObject={record.job} />
|
<OwnerNameDisplay ownerObject={record.job}/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<OwnerNameDisplay ownerObject={record.job} />
|
<OwnerNameDisplay ownerObject={record.job}/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.amount"),
|
title: t("payments.fields.amount"),
|
||||||
dataIndex: "amount",
|
dataIndex: "amount",
|
||||||
key: "amount",
|
key: "amount",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.memo"),
|
title: t("payments.fields.memo"),
|
||||||
dataIndex: "memo",
|
dataIndex: "memo",
|
||||||
key: "memo",
|
key: "memo",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.transactionid"),
|
title: t("payments.fields.transactionid"),
|
||||||
dataIndex: "transactionid",
|
dataIndex: "transactionid",
|
||||||
key: "transactionid",
|
key: "transactionid",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.created_at"),
|
title: t("payments.fields.created_at"),
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
key: "created_at",
|
key: "created_at",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.exportedat"),
|
title: t("payments.fields.exportedat"),
|
||||||
dataIndex: "exportedat",
|
dataIndex: "exportedat",
|
||||||
key: "exportedat",
|
key: "exportedat",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("exportlogs.labels.attempts"),
|
title: t("exportlogs.labels.attempts"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs} />
|
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<PaymentExportButton
|
<PaymentExportButton
|
||||||
paymentId={record.id}
|
paymentId={record.id}
|
||||||
disabled={transInProgress || !!record.exportedat}
|
disabled={transInProgress || !!record.exportedat}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
setSelectedPayments={setSelectedPayments}
|
setSelectedPayments={setSelectedPayments}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const handleSearch = (e) => {
|
||||||
setState({ ...state, search: e.target.value });
|
setState({...state, search: e.target.value});
|
||||||
logImEXEvent("account_payments_table_search");
|
logImEXEvent("account_payments_table_search");
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = state.search
|
const dataSource = state.search
|
||||||
? payments.filter(
|
? payments.filter(
|
||||||
(v) =>
|
(v) =>
|
||||||
(v.paymentnum || "")
|
(v.paymentnum || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
((v.job && v.job.ro_number) || "")
|
((v.job && v.job.ro_number) || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
((v.job && v.job.ownr_fn) || "")
|
((v.job && v.job.ownr_fn) || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
((v.job && v.job.ownr_ln) || "")
|
((v.job && v.job.ownr_ln) || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
((v.job && v.job.ownr_co_nm) || "")
|
((v.job && v.job.ownr_co_nm) || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase())
|
.includes(state.search.toLowerCase())
|
||||||
)
|
)
|
||||||
: payments;
|
: payments;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PaymentMarkSelectedExported
|
<PaymentMarkSelectedExported
|
||||||
paymentIds={selectedPayments}
|
paymentIds={selectedPayments}
|
||||||
disabled={transInProgress || selectedPayments.length === 0}
|
disabled={transInProgress || selectedPayments.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedPayments}
|
completedCallback={setSelectedPayments}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<PaymentsExportAllButton
|
<PaymentsExportAllButton
|
||||||
paymentIds={selectedPayments}
|
paymentIds={selectedPayments}
|
||||||
disabled={transInProgress || selectedPayments.length === 0}
|
disabled={transInProgress || selectedPayments.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedPayments}
|
completedCallback={setSelectedPayments}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
<QboAuthorizeComponent />
|
<QboAuthorizeComponent/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{position: "top", pageSize: pageLimit}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelectAll: (selected, selectedRows) =>
|
onSelectAll: (selected, selectedRows) =>
|
||||||
setSelectedPayments(selectedRows.map((i) => i.id)),
|
setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||||
},
|
},
|
||||||
getCheckboxProps: (record) => ({
|
getCheckboxProps: (record) => ({
|
||||||
disabled: record.exported,
|
disabled: record.exported,
|
||||||
}),
|
}),
|
||||||
selectedRowKeys: selectedPayments,
|
selectedRowKeys: selectedPayments,
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,247 +1,247 @@
|
|||||||
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 } from "../../utils/sorters";
|
import {alphaSort, dateSort} 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 QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import {DateFormatter} from "../../utils/DateFormatter";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(AccountingReceivablesTableComponent);
|
)(AccountingReceivablesTableComponent);
|
||||||
|
|
||||||
export function AccountingReceivablesTableComponent({
|
export function AccountingReceivablesTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
jobs,
|
jobs,
|
||||||
refetch,
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
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) => (
|
render: (text, record) => (
|
||||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
<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) => a.status - b.status,
|
sorter: (a, b) => a.status - b.status,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.date_invoiced"),
|
title: t("jobs.fields.date_invoiced"),
|
||||||
dataIndex: "date_invoiced",
|
dataIndex: "date_invoiced",
|
||||||
key: "date_invoiced",
|
key: "date_invoiced",
|
||||||
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "date_invoiced" &&
|
state.sortedInfo.columnKey === "date_invoiced" &&
|
||||||
state.sortedInfo.order,
|
state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DateFormatter>{record.date_invoiced}</DateFormatter>
|
<DateFormatter>{record.date_invoiced}</DateFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
key: "owner",
|
key: "owner",
|
||||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.owner ? (
|
return record.owner ? (
|
||||||
<Link to={"/manage/owners/" + record.owner.id}>
|
<Link to={"/manage/owners/" + record.owner.id}>
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
<OwnerNameDisplay ownerObject={record}/>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
<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,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.vehicleid ? (
|
return record.vehicleid ? (
|
||||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||||
record.v_model_desc || ""
|
record.v_model_desc || ""
|
||||||
}`}
|
}`}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||||
record.v_model_desc || ""
|
record.v_model_desc || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.clm_no"),
|
title: t("jobs.fields.clm_no"),
|
||||||
dataIndex: "clm_no",
|
dataIndex: "clm_no",
|
||||||
key: "clm_no",
|
key: "clm_no",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.clm_total"),
|
title: t("jobs.fields.clm_total"),
|
||||||
dataIndex: "clm_total",
|
dataIndex: "clm_total",
|
||||||
key: "clm_total",
|
key: "clm_total",
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("exportlogs.labels.attempts"),
|
title: t("exportlogs.labels.attempts"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs} />
|
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "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()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.ownr_fn || "")
|
(v.ownr_fn || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.ownr_ln || "")
|
(v.ownr_ln || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.ownr_co_nm || "")
|
(v.ownr_co_nm || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.v_model_desc || "")
|
(v.v_model_desc || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.v_make_desc || "")
|
(v.v_make_desc || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
)
|
)
|
||||||
: jobs;
|
: 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,72 @@
|
|||||||
import { Select, Button, Popover, InputNumber } 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 id="employeeSelector"
|
||||||
showSearch
|
showSearch
|
||||||
style={{ width: 200 }}
|
style={{width: 200}}
|
||||||
placeholder='Select a person'
|
placeholder='Select a person'
|
||||||
optionFilterProp='children'
|
optionFilterProp='children'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
filterOption={(input, option) =>
|
filterOption={(input, option) =>
|
||||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
}>
|
}>
|
||||||
{bodyshop.employees.map(emp => (
|
{bodyshop.employees.map(emp => (
|
||||||
<Select.Option value={emp.id} key={emp.id}>
|
<Select.Option value={emp.id} key={emp.id}>
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</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'
|
type='primary'
|
||||||
disabled={!assignment.employeeid}
|
disabled={!assignment.employeeid}
|
||||||
onClick={handleAssignment}>
|
onClick={handleAssignment}>
|
||||||
Assign
|
Assign
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} visible={visibility}>
|
<Popover content={popContent} open={visibility}>
|
||||||
<Button onClick={() => setVisibility(true)}>
|
<Button onClick={() => setVisibility(true)}>
|
||||||
{t("allocations.actions.assign")}
|
{t("allocations.actions.assign")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</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,47 @@
|
|||||||
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,
|
jobLineId,
|
||||||
hours,
|
hours,
|
||||||
refetch,
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const visibilityState = useState(false);
|
const visibilityState = useState(false);
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [assignment, setAssignment] = useState({
|
const [assignment, setAssignment] = useState({
|
||||||
joblineid: jobLineId,
|
joblineid: jobLineId,
|
||||||
hours: parseFloat(hours),
|
hours: parseFloat(hours),
|
||||||
employeeid: null,
|
employeeid: null,
|
||||||
});
|
});
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||||
|
|
||||||
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,68 @@
|
|||||||
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) => (
|
{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>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
disabled={!assignment.employeeid}
|
disabled={!assignment.employeeid}
|
||||||
onClick={handleAssignment}
|
onClick={handleAssignment}
|
||||||
>
|
>
|
||||||
Assign
|
Assign
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} visible={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,47 @@
|
|||||||
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,
|
jobLines,
|
||||||
refetch,
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const visibilityState = useState(false);
|
const visibilityState = useState(false);
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [assignment, setAssignment] = useState({
|
const [assignment, setAssignment] = useState({
|
||||||
employeeid: null,
|
employeeid: null,
|
||||||
});
|
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
|
||||||
|
|
||||||
const 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);
|
||||||
|
|
||||||
return (
|
const handleAssignment = () => {
|
||||||
<AllocationsBulkAssignment
|
const allocs = jobLines.reduce((acc, value) => {
|
||||||
disabled={jobLines.length > 0 ? false : true}
|
acc.push({
|
||||||
handleAssignment={handleAssignment}
|
joblineid: value.id,
|
||||||
assignment={assignment}
|
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||||
setAssignment={setAssignment}
|
employeeid: assignment.employeeid,
|
||||||
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,20 @@
|
|||||||
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.employee.last_name || ""
|
||||||
} (${allocation.hours || ""})`}
|
} (${allocation.hours || ""})`}
|
||||||
</span>
|
</span>
|
||||||
<Icon
|
<Icon
|
||||||
style={{ color: "red", padding: "0px 4px" }}
|
style={{color: "red", padding: "0px 4px"}}
|
||||||
component={MdRemoveCircleOutline}
|
component={MdRemoveCircleOutline}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
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
|
<AllocationsLabelComponent
|
||||||
allocation={allocation}
|
allocation={allocation}
|
||||||
handleClick={handleClick}
|
handleClick={handleClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,85 @@
|
|||||||
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,
|
sorter: (a, b) => a.created - b.created,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("audit.fields.operation"),
|
title: t("audit.fields.operation"),
|
||||||
dataIndex: "operation",
|
dataIndex: "operation",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("audit.fields.values"),
|
title: t("audit.fields.values"),
|
||||||
dataIndex: " old_val",
|
dataIndex: " old_val",
|
||||||
key: " old_val",
|
key: " old_val",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<AuditTrailValuesComponent
|
<AuditTrailValuesComponent
|
||||||
oldV={record.old_val}
|
oldV={record.old_val}
|
||||||
newV={record.new_val}
|
newV={record.new_val}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.useremail"),
|
||||||
|
dataIndex: "useremail",
|
||||||
|
key: "useremail",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: {
|
||||||
|
xs: {span: 12},
|
||||||
|
sm: {span: 5},
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
xs: {span: 24},
|
||||||
|
sm: {span: 12},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
{...formItemLayout}
|
||||||
|
loading={loading}
|
||||||
|
pagination={{position: "top", defaultPageSize: pageLimit}}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data}
|
||||||
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("audit.fields.useremail"),
|
|
||||||
dataIndex: "useremail",
|
|
||||||
key: "useremail",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const formItemLayout = {
|
|
||||||
labelCol: {
|
|
||||||
xs: { span: 12 },
|
|
||||||
sm: { span: 5 },
|
|
||||||
},
|
|
||||||
wrapperCol: {
|
|
||||||
xs: { span: 24 },
|
|
||||||
sm: { span: 12 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
{...formItemLayout}
|
|
||||||
loading={loading}
|
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
dataSource={data}
|
|
||||||
onChange={handleTableChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
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}
|
loading={loading}
|
||||||
data={data ? data.audit_trail : []}
|
data={data ? data.audit_trail : []}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<EmailAuditTrailListComponent
|
<EmailAuditTrailListComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.audit_trail : []}
|
data={data ? data.audit_trail : []}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
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,
|
sorter: (a, b) => a.created - b.created,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("audit.fields.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,29 +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 }) {
|
|
||||||
if (!oldV && !newV) return <div></div>;
|
export default function AuditTrailValuesComponent({oldV, newV}) {
|
||||||
|
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(newV).map((key, idx) => (
|
{Object.keys(oldV).map((key, idx) => (
|
||||||
<List.Item key={idx} value={key}>
|
<List.Item key={idx}>
|
||||||
{key}: {JSON.stringify(newV[key])}
|
{key}: {oldV[key]} <Icon component={FaArrowRight}/>
|
||||||
</List.Item>
|
{JSON.stringify(newV[key])}
|
||||||
))}
|
</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,22 +1,23 @@
|
|||||||
import { Tag, Popover } 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 }) {
|
|
||||||
const { t } = useTranslation();
|
export default function BarcodePopupComponent({value, children}) {
|
||||||
return (
|
const {t} = useTranslation();
|
||||||
<div>
|
return (
|
||||||
<Popover
|
<div>
|
||||||
content={
|
<Popover
|
||||||
<Barcode
|
content={
|
||||||
value={value || ""}
|
<Barcode
|
||||||
background="transparent"
|
value={value || ""}
|
||||||
displayValue={false}
|
background="transparent"
|
||||||
/>
|
displayValue={false}
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
>
|
||||||
</Popover>
|
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||||
</div>
|
</Popover>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,136 @@
|
|||||||
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,
|
form,
|
||||||
returnLoading,
|
returnLoading,
|
||||||
returnData,
|
returnData,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (returnData) {
|
if (returnData) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
outstanding_returns: returnData.parts_order_lines,
|
outstanding_returns: returnData.parts_order_lines,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}, [returnData, form]);
|
|
||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
if (returnLoading) return <Skeleton />;
|
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");
|
||||||
|
|
||||||
return (
|
if (!isReturn) {
|
||||||
<Form.List name="outstanding_returns">
|
return null;
|
||||||
{(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>
|
|
||||||
|
|
||||||
<td>
|
if (returnLoading) return <Skeleton/>;
|
||||||
<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>
|
return (
|
||||||
<Form.Item
|
<Form.List name="outstanding_returns">
|
||||||
span={2}
|
{(fields, {add, remove, move}) => {
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
return (
|
||||||
key={`${index}cm_received`}
|
<>
|
||||||
name={[field.name, "cm_received"]}
|
<Typography.Title level={4}>
|
||||||
valuePropName="checked"
|
{t("bills.labels.creditsnotreceived")}
|
||||||
>
|
</Typography.Title>
|
||||||
<Checkbox />
|
<table className="bill-cm-returns-table">
|
||||||
</Form.Item>
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th>{t("parts_orders.fields.line_desc")}</th>
|
||||||
))}
|
<th>{t("parts_orders.fields.part_type")}</th>
|
||||||
</tbody>
|
<th>{t("parts_orders.fields.quantity")}</th>
|
||||||
</table>
|
<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>
|
||||||
|
|
||||||
|
<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.List>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
.bill-cm-returns-table {
|
.bill-cm-returns-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,80 +1,80 @@
|
|||||||
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";
|
||||||
|
|
||||||
export default function BillDeleteButton({ bill, callback }) {
|
export default function BillDeleteButton({bill, callback}) {
|
||||||
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")});
|
||||||
|
|
||||||
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}
|
disabled={bill.exported}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
title={t("bills.labels.deleteconfirm")}
|
title={t("bills.labels.deleteconfirm")}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
disabled={bill.exported}
|
disabled={bill.exported}
|
||||||
// onClick={handleDelete}
|
// onClick={handleDelete}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
<DeleteFilled />
|
<DeleteFilled/>
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import {useMutation, useQuery} from "@apollo/client";
|
||||||
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
|
import {Button, Form, Popconfirm, Space} from "antd";
|
||||||
import moment from "moment";
|
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 {
|
import {DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE} from "../../graphql/bill-lines.queries";
|
||||||
DELETE_BILL_LINE,
|
import {QUERY_BILL_BY_PK, UPDATE_BILL} from "../../graphql/bills.queries";
|
||||||
INSERT_NEW_BILL_LINES,
|
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||||
UPDATE_BILL_LINE,
|
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||||
} from "../../graphql/bill-lines.queries";
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import 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";
|
||||||
@@ -26,227 +22,226 @@ 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";
|
||||||
|
|
||||||
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 }) =>
|
insertAuditTrail: ({jobid, operation}) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({jobid, operation})),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillDetailEditcontainer);
|
)(BillDetailEditcontainer);
|
||||||
|
|
||||||
export function BillDetailEditcontainer({
|
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
|
||||||
setPartsOrderContext,
|
const search = queryString.parse(useLocation().search);
|
||||||
insertAuditTrail,
|
|
||||||
bodyshop,
|
|
||||||
}) {
|
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visible, setVisible] = 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",
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
//It's got a previously deducted bill line!
|
|
||||||
if (
|
|
||||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
|
||||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
|
||||||
0
|
|
||||||
)
|
|
||||||
setVisible(true);
|
|
||||||
else {
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
setUpdateLoading(true);
|
|
||||||
//let adjustmentsToInsert = {};
|
|
||||||
|
|
||||||
const { billlines, upload, ...bill } = values;
|
|
||||||
const updates = [];
|
|
||||||
updates.push(
|
|
||||||
update_bill({
|
|
||||||
variables: { billId: search.billid, bill: bill },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
billlines.forEach((l) => {
|
|
||||||
delete l.selected;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Find bill lines that were deleted.
|
// ... rest of the code remains the same
|
||||||
const deletedJobLines = [];
|
|
||||||
|
|
||||||
data.bills_by_pk.billlines.forEach((a) => {
|
const handleSave = () => {
|
||||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
//It's got a previously deducted bill line!
|
||||||
if (!matchingRecord) {
|
if (
|
||||||
deletedJobLines.push(a);
|
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||||
}
|
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
||||||
});
|
0
|
||||||
|
)
|
||||||
|
setOpen(true);
|
||||||
|
else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
deletedJobLines.forEach((d) => {
|
const handleFinish = async (values) => {
|
||||||
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
setUpdateLoading(true);
|
||||||
});
|
//let adjustmentsToInsert = {};
|
||||||
|
|
||||||
billlines.forEach((billline) => {
|
const {billlines, upload, ...bill} = values;
|
||||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
const updates = [];
|
||||||
delete il.__typename;
|
|
||||||
|
|
||||||
if (il.id) {
|
|
||||||
updates.push(
|
updates.push(
|
||||||
updateBillLine({
|
update_bill({
|
||||||
variables: {
|
variables: {billId: search.billid, bill: bill},
|
||||||
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);
|
billlines.forEach((l) => {
|
||||||
|
delete l.selected;
|
||||||
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
//Find bill lines that were deleted.
|
||||||
jobid: bill.jobid,
|
const deletedJobLines = [];
|
||||||
billid: search.billid,
|
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
data.bills_by_pk.billlines.forEach((a) => {
|
||||||
form.setFieldsValue(transformData(data));
|
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||||
form.resetFields();
|
if (!matchingRecord) {
|
||||||
setVisible(false);
|
deletedJobLines.push(a);
|
||||||
setUpdateLoading(false);
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
deletedJobLines.forEach((d) => {
|
||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
updates.push(deleteBillLine({variables: {id: d.id}}));
|
||||||
|
});
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
billlines.forEach((billline) => {
|
||||||
|
const {deductedfromlbr, inventories, jobline, ...il} = billline;
|
||||||
|
delete il.__typename;
|
||||||
|
|
||||||
return (
|
if (il.id) {
|
||||||
<>
|
updates.push(
|
||||||
{loading && <LoadingSkeleton />}
|
updateBillLine({
|
||||||
{data && (
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
{loading && <LoadingSkeleton/>}
|
||||||
title={
|
{data && (
|
||||||
data &&
|
<>
|
||||||
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
<PageHeader
|
||||||
}
|
title={
|
||||||
extra={
|
data &&
|
||||||
<Space>
|
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
||||||
<BillDetailEditReturn data={data} />
|
}
|
||||||
<BillPrintButton billid={search.billid} />
|
extra={
|
||||||
<Popconfirm
|
<Space>
|
||||||
visible={visible}
|
<BillDetailEditReturn data={data}/>
|
||||||
onConfirm={() => form.submit()}
|
<BillPrintButton billid={search.billid}/>
|
||||||
onCancel={() => setVisible(false)}
|
<Popconfirm
|
||||||
okButtonProps={{ loading: updateLoading }}
|
open={open}
|
||||||
title={t("bills.labels.editadjwarning")}
|
onConfirm={() => form.submit()}
|
||||||
>
|
onCancel={() => setOpen(false)}
|
||||||
<Button
|
okButtonProps={{loading: updateLoading}}
|
||||||
htmlType="submit"
|
title={t("bills.labels.editadjwarning")}
|
||||||
disabled={exported}
|
>
|
||||||
onClick={handleSave}
|
<Button
|
||||||
loading={updateLoading}
|
htmlType="submit"
|
||||||
type="primary"
|
disabled={exported}
|
||||||
>
|
onClick={handleSave}
|
||||||
{t("general.actions.save")}
|
loading={updateLoading}
|
||||||
</Button>
|
type="primary"
|
||||||
</Popconfirm>
|
>
|
||||||
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
{t("general.actions.save")}
|
||||||
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
</Button>
|
||||||
</Space>
|
</Popconfirm>
|
||||||
}
|
<BillReeportButtonComponent bill={data && data.bills_by_pk}/>
|
||||||
/>
|
<BillMarkExportedButton bill={data && data.bills_by_pk}/>
|
||||||
<Form
|
</Space>
|
||||||
form={form}
|
}
|
||||||
onFinish={handleFinish}
|
/>
|
||||||
initialValues={transformData(data)}
|
<Form
|
||||||
layout="vertical"
|
form={form}
|
||||||
>
|
onFinish={handleFinish}
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
initialValues={transformData(data)}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<BillFormContainer form={form} billEdit disabled={exported}/>
|
||||||
|
|
||||||
{bodyshop.uselocalmediaserver ? (
|
{bodyshop.uselocalmediaserver ? (
|
||||||
<JobsDocumentsLocalGallery
|
<JobsDocumentsLocalGallery
|
||||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
job={{id: data ? data.bills_by_pk.jobid : null}}
|
||||||
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||||
vendorid={data ? data.bills_by_pk.vendorid : null}
|
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<JobDocumentsGallery
|
<JobDocumentsGallery
|
||||||
jobId={data ? data.bills_by_pk.jobid : null}
|
jobId={data ? data.bills_by_pk.jobid : null}
|
||||||
billId={search.billid}
|
billId={search.billid}
|
||||||
documentsList={data ? data.bills_by_pk.documents : []}
|
documentsList={data ? data.bills_by_pk.documents : []}
|
||||||
billsCallback={refetch}
|
billsCallback={refetch}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</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 ? moment(data.bills_by_pk.date) : null,
|
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,185 +1,185 @@
|
|||||||
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 { useHistory, useLocation } 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 }) =>
|
insertAuditTrail: ({jobid, operation}) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({jobid, operation})),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillDetailEditReturn);
|
)(BillDetailEditReturn);
|
||||||
|
|
||||||
export function BillDetailEditReturn({
|
export function BillDetailEditReturn({
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
insertAuditTrail,
|
insertAuditTrail,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
data,
|
data,
|
||||||
disabled,
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useNavigate();
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visible, setVisible] = useState(false);
|
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.push({ search: queryString.stringify(search) });
|
history({search: queryString.stringify(search)});
|
||||||
setVisible(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible === false) form.resetFields();
|
if (open === false) form.resetFields();
|
||||||
}, [visible, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
open={open}
|
||||||
onCancel={() => setVisible(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}
|
initialValues={data && data.bills_by_pk}
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
form={form}
|
form={form}
|
||||||
>
|
>
|
||||||
<Form.List name={["billlines"]}>
|
<Form.List name={["billlines"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, {add, remove, move}) => {
|
||||||
return (
|
return (
|
||||||
<table style={{ tableLayout: "auto", width: "100%" }}>
|
<table style={{tableLayout: "auto", width: "100%"}}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
billlines: form
|
billlines: form
|
||||||
.getFieldsValue()
|
.getFieldsValue()
|
||||||
.billlines.map((b) => ({
|
.billlines.map((b) => ({
|
||||||
...b,
|
...b,
|
||||||
selected: e.target.checked,
|
selected: e.target.checked,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{t("billlines.fields.line_desc")}</td>
|
<td>{t("billlines.fields.line_desc")}</td>
|
||||||
<td>{t("billlines.fields.quantity")}</td>
|
<td>{t("billlines.fields.quantity")}</td>
|
||||||
<td>{t("billlines.fields.actual_price")}</td>
|
<td>{t("billlines.fields.actual_price")}</td>
|
||||||
<td>{t("billlines.fields.actual_cost")}</td>
|
<td>{t("billlines.fields.actual_cost")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<tr key={field.key}>
|
<tr key={field.key}>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.selected")}
|
// label={t("joblines.fields.selected")}
|
||||||
key={`${index}selected`}
|
key={`${index}selected`}
|
||||||
name={[field.name, "selected"]}
|
name={[field.name, "selected"]}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Checkbox />
|
<Checkbox/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.line_desc")}
|
// label={t("joblines.fields.line_desc")}
|
||||||
key={`${index}line_desc`}
|
key={`${index}line_desc`}
|
||||||
name={[field.name, "line_desc"]}
|
name={[field.name, "line_desc"]}
|
||||||
>
|
>
|
||||||
<ReadOnlyFormItemComponent />
|
<ReadOnlyFormItemComponent/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.quantity")}
|
// label={t("joblines.fields.quantity")}
|
||||||
key={`${index}quantity`}
|
key={`${index}quantity`}
|
||||||
name={[field.name, "quantity"]}
|
name={[field.name, "quantity"]}
|
||||||
>
|
>
|
||||||
<ReadOnlyFormItemComponent />
|
<ReadOnlyFormItemComponent/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.actual_price")}
|
// label={t("joblines.fields.actual_price")}
|
||||||
key={`${index}actual_price`}
|
key={`${index}actual_price`}
|
||||||
name={[field.name, "actual_price"]}
|
name={[field.name, "actual_price"]}
|
||||||
>
|
>
|
||||||
<ReadOnlyFormItemComponent type="currency" />
|
<ReadOnlyFormItemComponent type="currency"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
// label={t("joblines.fields.actual_cost")}
|
// label={t("joblines.fields.actual_cost")}
|
||||||
key={`${index}actual_cost`}
|
key={`${index}actual_cost`}
|
||||||
name={[field.name, "actual_cost"]}
|
name={[field.name, "actual_cost"]}
|
||||||
>
|
>
|
||||||
<ReadOnlyFormItemComponent type="currency" />
|
<ReadOnlyFormItemComponent type="currency"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button
|
<Button
|
||||||
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("bills.actions.return")}
|
{t("bills.actions.return")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
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 { useHistory, useLocation } 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 = useHistory();
|
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]]
|
? bpoints[selectedBreakpoint[0]]
|
||||||
: "100%";
|
: "100%";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width={drawerPercentage}
|
width={drawerPercentage}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
delete search.billid;
|
delete search.billid;
|
||||||
history.push({ search: queryString.stringify(search) });
|
history({search: queryString.stringify(search)});
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
visible={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,470 +1,466 @@
|
|||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import {useApolloClient, useMutation} from "@apollo/client";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
import { Button, Checkbox, Form, Modal, Space, notification } 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 {
|
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries";
|
||||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries";
|
||||||
UPDATE_JOB,
|
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||||
} from "../../graphql/jobs.queries";
|
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||||
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
|
||||||
import {
|
|
||||||
selectBodyshop,
|
|
||||||
selectCurrentUser,
|
|
||||||
} from "../../redux/user/user.selectors";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import 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 }) =>
|
insertAuditTrail: ({jobid, billid, operation}) =>
|
||||||
dispatch(insertAuditTrail({ jobid, billid, operation })),
|
dispatch(insertAuditTrail({jobid, billid, operation})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const Templates = TemplateList("job_special");
|
const Templates = TemplateList("job_special");
|
||||||
|
|
||||||
function BillEnterModalContainer({
|
function BillEnterModalContainer({
|
||||||
billEnterModal,
|
billEnterModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
currentUser,
|
currentUser,
|
||||||
insertAuditTrail,
|
insertAuditTrail,
|
||||||
}) {
|
}) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||||
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||||
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [generateLabel, setGenerateLabel] = useLocalStorage(
|
const [generateLabel, setGenerateLabel] = useLocalStorage(
|
||||||
"enter_bill_generate_label",
|
"enter_bill_generate_label",
|
||||||
false
|
false
|
||||||
);
|
|
||||||
const { Enhanced_Payroll } = useTreatments(
|
|
||||||
["Enhanced_Payroll"],
|
|
||||||
{},
|
|
||||||
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,
|
|
||||||
...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"],
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
payrollAdjustmentsToInsert.map((li) => {
|
|
||||||
return updateJobLines({
|
|
||||||
variables: {
|
|
||||||
lineId: li.id,
|
|
||||||
line: {
|
|
||||||
convertedtolbr: li.convertedtolbr,
|
|
||||||
convertedtolbr_data: li.convertedtolbr_data,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
|
||||||
if (adjKeys.length > 0) {
|
attributes: {},
|
||||||
//Query the adjustments, merge, and update them.
|
names: ["Enhanced_Payroll"],
|
||||||
const existingAdjustments = await client.query({
|
splitKey: bodyshop.imexshopid,
|
||||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
});
|
||||||
variables: {
|
|
||||||
id: values.jobid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const newAdjustments = _.cloneDeep(
|
const formValues = useMemo(() => {
|
||||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
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]);
|
||||||
|
|
||||||
adjKeys.forEach((key) => {
|
const handleFinish = async (values) => {
|
||||||
newAdjustments[key] =
|
let totals = CalculateBillTotal(values);
|
||||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
if (totals.discrepancy.getAmount() !== 0) {
|
||||||
|
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const {
|
||||||
|
upload,
|
||||||
|
location,
|
||||||
|
outstanding_returns,
|
||||||
|
inventory,
|
||||||
|
...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"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
payrollAdjustmentsToInsert.map((li) => {
|
||||||
|
return updateJobLines({
|
||||||
|
variables: {
|
||||||
|
lineId: li.id,
|
||||||
|
line: {
|
||||||
|
convertedtolbr: li.convertedtolbr,
|
||||||
|
convertedtolbr_data: li.convertedtolbr_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
|
if (adjKeys.length > 0) {
|
||||||
|
//Query the adjustments, merge, and update them.
|
||||||
|
const existingAdjustments = await client.query({
|
||||||
|
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
|
variables: {
|
||||||
|
id: values.jobid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newAdjustments = _.cloneDeep(
|
||||||
|
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||||
|
);
|
||||||
|
|
||||||
|
adjKeys.forEach((key) => {
|
||||||
|
newAdjustments[key] =
|
||||||
|
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: values.jobid,
|
||||||
|
operation: AuditTrailMapping.jobmodifylbradj({
|
||||||
|
mod_lbr_ty: key,
|
||||||
|
hours: adjustmentsToInsert[key].toFixed(1),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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)},
|
||||||
|
});
|
||||||
|
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,
|
||||||
operation: AuditTrailMapping.jobmodifylbradj({
|
billid: billId,
|
||||||
mod_lbr_ty: key,
|
operation: AuditTrailMapping.billposted(
|
||||||
hours: adjustmentsToInsert[key].toFixed(1),
|
r1.data.insert_bills.returning[0].invoice_number
|
||||||
}),
|
),
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const jobUpdate = client.mutate({
|
if (enterAgain) {
|
||||||
mutation: UPDATE_JOB,
|
// form.resetFields();
|
||||||
variables: {
|
form.setFieldsValue({
|
||||||
jobId: values.jobid,
|
...formValues,
|
||||||
job: { lbr_adjustments: newAdjustments },
|
billlines: [],
|
||||||
},
|
|
||||||
});
|
|
||||||
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) },
|
|
||||||
});
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})
|
form.resetFields();
|
||||||
);
|
} else {
|
||||||
}
|
toggleModalVisible();
|
||||||
|
}
|
||||||
|
setEnterAgain(false);
|
||||||
|
};
|
||||||
|
|
||||||
/////////////////////////
|
const handleCancel = () => {
|
||||||
if (upload && upload.length > 0) {
|
const r = window.confirm(t("general.labels.cancel"));
|
||||||
//insert Each of the documents?
|
if (r === true) {
|
||||||
|
toggleModalVisible();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (bodyshop.uselocalmediaserver) {
|
useEffect(() => {
|
||||||
upload.forEach((u) => {
|
if (enterAgain) form.submit();
|
||||||
handleLocalUpload({
|
}, [enterAgain, form]);
|
||||||
ev: { file: u.originFileObj },
|
|
||||||
context: {
|
useEffect(() => {
|
||||||
jobid: values.jobid,
|
if (billEnterModal.open) {
|
||||||
invoice_number: remainingValues.invoice_number,
|
form.setFieldsValue(formValues);
|
||||||
vendorid: remainingValues.vendorid,
|
} else {
|
||||||
},
|
form.resetFields();
|
||||||
});
|
}
|
||||||
});
|
}, [billEnterModal.open, form, formValues]);
|
||||||
} else {
|
|
||||||
upload.forEach((u) => {
|
return (
|
||||||
handleUpload(
|
<Modal
|
||||||
{ file: u.originFileObj },
|
title={t("bills.labels.new")}
|
||||||
{
|
width={"98%"}
|
||||||
bodyshop: bodyshop,
|
open={billEnterModal.open}
|
||||||
uploaded_by: currentUser.email,
|
okText={t("general.actions.save")}
|
||||||
jobId: values.jobid,
|
keyboard="false"
|
||||||
billId: billId,
|
onOk={() => form.submit()}
|
||||||
tagsArray: null,
|
onCancel={handleCancel}
|
||||||
callback: null,
|
afterClose={() => {
|
||||||
|
form.resetFields();
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={generateLabel}
|
||||||
|
onChange={(e) => setGenerateLabel(e.target.checked)}
|
||||||
|
>
|
||||||
|
{t("bills.labels.generatepartslabel")}
|
||||||
|
</Checkbox>
|
||||||
|
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||||
|
<Button loading={loading} onClick={() => form.submit()}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => {
|
||||||
|
setEnterAgain(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.actions.saveandnew")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
);
|
destroyOnClose
|
||||||
});
|
>
|
||||||
}
|
<Form
|
||||||
}
|
onFinish={handleFinish}
|
||||||
///////////////////////////
|
autoComplete={"off"}
|
||||||
setLoading(false);
|
layout="vertical"
|
||||||
notification["success"]({
|
form={form}
|
||||||
message: t("bills.successes.created"),
|
onFinishFailed={() => {
|
||||||
});
|
setEnterAgain(false);
|
||||||
|
}}
|
||||||
if (generateLabel) {
|
|
||||||
GenerateDocument(
|
|
||||||
{
|
|
||||||
name: Templates.parts_invoice_label_single.key,
|
|
||||||
variables: {
|
|
||||||
id: billId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
"p"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
|
||||||
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: values.jobid,
|
|
||||||
billid: billId,
|
|
||||||
operation: AuditTrailMapping.billposted(
|
|
||||||
r1.data.insert_bills.returning[0].invoice_number
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enterAgain) {
|
|
||||||
// form.resetFields();
|
|
||||||
form.setFieldsValue({
|
|
||||||
...formValues,
|
|
||||||
billlines: [],
|
|
||||||
});
|
|
||||||
form.resetFields();
|
|
||||||
} else {
|
|
||||||
toggleModalVisible();
|
|
||||||
}
|
|
||||||
setEnterAgain(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
const r = window.confirm(t("general.labels.cancel"));
|
|
||||||
if (r === true) {
|
|
||||||
toggleModalVisible();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (enterAgain) form.submit();
|
|
||||||
}, [enterAgain, form]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (billEnterModal.visible) {
|
|
||||||
form.setFieldsValue(formValues);
|
|
||||||
} else {
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}, [billEnterModal.visible, form, formValues]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t("bills.labels.new")}
|
|
||||||
width={"98%"}
|
|
||||||
visible={billEnterModal.visible}
|
|
||||||
okText={t("general.actions.save")}
|
|
||||||
keyboard="false"
|
|
||||||
onOk={() => form.submit()}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
afterClose={() => {
|
|
||||||
form.resetFields();
|
|
||||||
setLoading(false);
|
|
||||||
}}
|
|
||||||
footer={
|
|
||||||
<Space>
|
|
||||||
<Checkbox
|
|
||||||
checked={generateLabel}
|
|
||||||
onChange={(e) => setGenerateLabel(e.target.checked)}
|
|
||||||
>
|
|
||||||
{t("bills.labels.generatepartslabel")}
|
|
||||||
</Checkbox>
|
|
||||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
|
||||||
{t("general.actions.save")}
|
|
||||||
</Button>
|
|
||||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={() => {
|
|
||||||
setEnterAgain(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("general.actions.saveandnew")}
|
<BillFormContainer
|
||||||
</Button>
|
form={form}
|
||||||
)}
|
disableInvNumber={billEnterModal.context.disableInvNumber}
|
||||||
</Space>
|
/>
|
||||||
}
|
</Form>
|
||||||
destroyOnClose
|
</Modal>
|
||||||
>
|
);
|
||||||
<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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillEnterModalContainer);
|
)(BillEnterModalContainer);
|
||||||
|
|||||||
@@ -1,135 +1,136 @@
|
|||||||
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,
|
lineData,
|
||||||
discount,
|
discount,
|
||||||
form,
|
form,
|
||||||
responsibilityCenters,
|
responsibilityCenters,
|
||||||
disabled,
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
title: t("joblines.fields.line_desc"),
|
|
||||||
dataIndex: "line_desc",
|
|
||||||
key: "line_desc",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("joblines.fields.oem_partno"),
|
|
||||||
dataIndex: "oem_partno",
|
|
||||||
key: "oem_partno",
|
|
||||||
width: "10%",
|
|
||||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("joblines.fields.part_type"),
|
|
||||||
dataIndex: "part_type",
|
|
||||||
key: "part_type",
|
|
||||||
width: "10%",
|
|
||||||
filters: [
|
|
||||||
{
|
{
|
||||||
text: t("jobs.labels.partsfilter"),
|
title: t("joblines.fields.line_desc"),
|
||||||
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
|
dataIndex: "line_desc",
|
||||||
|
key: "line_desc",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t("joblines.fields.part_types.PAN"),
|
title: t("joblines.fields.oem_partno"),
|
||||||
value: ["PAN", "PAP"],
|
dataIndex: "oem_partno",
|
||||||
|
key: "oem_partno",
|
||||||
|
width: "10%",
|
||||||
|
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t("joblines.fields.part_types.PAL"),
|
title: t("joblines.fields.part_type"),
|
||||||
value: ["PAL"],
|
dataIndex: "part_type",
|
||||||
|
key: "part_type",
|
||||||
|
width: "10%",
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: t("jobs.labels.partsfilter"),
|
||||||
|
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAN"),
|
||||||
|
value: ["PAN", "PAP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAL"),
|
||||||
|
value: ["PAL"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAA"),
|
||||||
|
value: ["PAA"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t("joblines.fields.part_types.PAS"),
|
||||||
|
value: ["PAS", "PASL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFilter: (value, record) => value.includes(record.part_type),
|
||||||
|
render: (text, record) =>
|
||||||
|
record.part_type
|
||||||
|
? t(`joblines.fields.part_types.${record.part_type}`)
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: t("joblines.fields.part_types.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.prt_dsmk_m
|
||||||
: record.act_price}
|
: record.act_price}
|
||||||
</CurrencyFormatter>
|
</CurrencyFormatter>
|
||||||
{record.part_qty ? `(x ${record.part_qty})` : null}
|
{record.part_qty ? `(x ${record.part_qty})` : null}
|
||||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||||
<span
|
<span
|
||||||
style={{ marginLeft: ".2rem" }}
|
style={{marginLeft: ".2rem"}}
|
||||||
>{`(${record.prt_dsmk_p}%)`}</span>
|
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.posting"),
|
title: t("billlines.fields.posting"),
|
||||||
dataIndex: "posting",
|
dataIndex: "posting",
|
||||||
key: "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,288 +1,284 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons";
|
||||||
PlusCircleFilled,
|
import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd";
|
||||||
MinusCircleFilled,
|
import {useTranslation} from "react-i18next";
|
||||||
WarningOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Form, Button, InputNumber, Input, Select, Switch, Space } from "antd";
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillFormItemsExtendedFormItem);
|
)(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 (
|
||||||
<Button
|
<Space wrap>
|
||||||
onClick={() => {
|
<Form.Item
|
||||||
const values = form.getFieldsValue("billlineskeys");
|
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);
|
||||||
|
|
||||||
form.setFieldsValue({
|
if (lineDiscount - discount === 0) return <div/>;
|
||||||
...values,
|
return <WarningOutlined style={{color: "red"}}/>;
|
||||||
billlineskeys: {
|
}}
|
||||||
...(values.billlineskeys || {}),
|
</Form.Item>
|
||||||
[record.id]: {
|
<Form.Item
|
||||||
joblineid: record.id,
|
label={t("billlines.fields.cost_center")}
|
||||||
line_desc: record.line_desc,
|
name={["billlineskeys", record.id, "cost_center"]}
|
||||||
quantity: record.part_qty || 1,
|
>
|
||||||
actual_price: record.act_price,
|
<Select showSearch style={{minWidth: "3rem"}} disabled={disabled}>
|
||||||
cost_center: record.part_type
|
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
? CiecaSelect(true, false)
|
||||||
? record.part_type
|
: responsibilityCenters.costs.map((item) => (
|
||||||
: responsibilityCenters.defaults &&
|
<Select.Option key={item.name}>{item.name}</Select.Option>
|
||||||
(responsibilityCenters.defaults.costs[record.part_type] ||
|
))}
|
||||||
null)
|
</Select>
|
||||||
: null,
|
</Form.Item>
|
||||||
},
|
<Form.Item
|
||||||
},
|
label={t("billlines.fields.location")}
|
||||||
});
|
name={["billlineskeys", record.id, "location"]}
|
||||||
}}
|
>
|
||||||
>
|
<Select disabled={disabled}>
|
||||||
<PlusCircleFilled />
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||||
</Button>
|
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +1,83 @@
|
|||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import {useLazyQuery, useQuery} from "@apollo/client";
|
||||||
import { useTreatments } 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,
|
bodyshop,
|
||||||
form,
|
form,
|
||||||
billEdit,
|
billEdit,
|
||||||
disabled,
|
disabled,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
}) {
|
}) {
|
||||||
const { Simple_Inventory } = useTreatments(
|
const {treatments: {Simple_Inventory}} = useSplitTreatments({
|
||||||
["Simple_Inventory"],
|
attributes: {},
|
||||||
{},
|
names: ["Simple_Inventory"],
|
||||||
bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid,
|
||||||
);
|
});
|
||||||
|
|
||||||
const { data: VendorAutoCompleteData } = useQuery(
|
const {data: VendorAutoCompleteData} = useQuery(
|
||||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||||
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
{fetchPolicy: "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 }] =
|
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
|
||||||
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
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}
|
loadLines={loadLines}
|
||||||
lineData={lineData ? lineData.joblines : []}
|
lineData={lineData ? lineData.joblines : []}
|
||||||
job={lineData ? lineData.jobs_by_pk : null}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
loadInventory={loadInventory}
|
loadInventory={loadInventory}
|
||||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||||
/>
|
/>
|
||||||
{!billEdit && (
|
{!billEdit && (
|
||||||
<BillCmdReturnsTableComponent
|
<BillCmdReturnsTableComponent
|
||||||
form={form}
|
form={form}
|
||||||
returnLoading={returnLoading}
|
returnLoading={returnLoading}
|
||||||
returnData={returnData}
|
returnData={returnData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Simple_Inventory.treatment === "on" && (
|
{Simple_Inventory.treatment === "on" && (
|
||||||
<BillInventoryTable
|
<BillInventoryTable
|
||||||
form={form}
|
form={form}
|
||||||
inventoryLoading={inventoryLoading}
|
inventoryLoading={inventoryLoading}
|
||||||
inventoryData={billEdit ? [] : inventoryData}
|
inventoryData={billEdit ? [] : inventoryData}
|
||||||
billEdit={billEdit}
|
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,47 @@
|
|||||||
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)
|
if (i.applicable_taxes?.state)
|
||||||
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
|
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
|
||||||
if (i.applicable_taxes?.local)
|
if (i.applicable_taxes?.local)
|
||||||
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
|
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,173 @@
|
|||||||
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,
|
billEnterModal,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
form,
|
form,
|
||||||
billEdit,
|
billEdit,
|
||||||
inventoryLoading,
|
inventoryLoading,
|
||||||
inventoryData,
|
inventoryData,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
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]);
|
||||||
|
|
||||||
if (inventoryLoading) return <Skeleton />;
|
return (
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const is_inhouse =
|
||||||
|
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||||
|
|
||||||
return (
|
if (!is_inhouse || billEdit) {
|
||||||
<Form.List name="inventory">
|
return null;
|
||||||
{(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>
|
|
||||||
|
|
||||||
<td>
|
if (inventoryLoading) return <Skeleton/>;
|
||||||
<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>
|
return (
|
||||||
<Form.Item
|
<Form.List name="inventory">
|
||||||
span={2}
|
{(fields, {add, remove, move}) => {
|
||||||
//label={t("joblines.fields.mod_lb_hrs")}
|
return (
|
||||||
key={`${index}consumefrominventory`}
|
<>
|
||||||
name={[field.name, "consumefrominventory"]}
|
<Typography.Title level={4}>
|
||||||
valuePropName="checked"
|
{t("inventory.labels.inventory")}
|
||||||
>
|
</Typography.Title>
|
||||||
<Checkbox />
|
<table className="bill-inventory-table">
|
||||||
</Form.Item>
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th>{t("billlines.fields.line_desc")}</th>
|
||||||
))}
|
<th>{t("vendors.fields.name")}</th>
|
||||||
</tbody>
|
<th>{t("billlines.fields.quantity")}</th>
|
||||||
</table>
|
<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>
|
||||||
|
|
||||||
|
<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.List>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
.bill-inventory-table {
|
.bill-inventory-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,83 +1,87 @@
|
|||||||
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";
|
||||||
|
|
||||||
//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 },
|
{options, disabled, allowRemoved, ...restProps},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
dropdownMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
// optionFilterProp="line_desc"
|
optionLabelProp={"name"}
|
||||||
filterOption={(inputValue, option) => {
|
// optionFilterProp="line_desc"
|
||||||
return (
|
filterOption={(inputValue, option) => {
|
||||||
(option.line_desc &&
|
return (
|
||||||
option.line_desc
|
(option.line_desc &&
|
||||||
.toLowerCase()
|
option.line_desc
|
||||||
.includes(inputValue.toLowerCase())) ||
|
.toLowerCase()
|
||||||
(option.oem_partno &&
|
.includes(inputValue.toLowerCase())) ||
|
||||||
option.oem_partno
|
(option.oem_partno &&
|
||||||
.toLowerCase()
|
option.oem_partno
|
||||||
.includes(inputValue.toLowerCase())) ||
|
.toLowerCase()
|
||||||
(option.alt_partno &&
|
.includes(inputValue.toLowerCase())) ||
|
||||||
option.alt_partno
|
(option.alt_partno &&
|
||||||
.toLowerCase()
|
option.alt_partno
|
||||||
.includes(inputValue.toLowerCase())) ||
|
.toLowerCase()
|
||||||
(option.act_price &&
|
.includes(inputValue.toLowerCase())) ||
|
||||||
option.act_price.toString().startsWith(inputValue.toString()))
|
(option.act_price &&
|
||||||
);
|
option.act_price.toString().startsWith(inputValue.toString()))
|
||||||
}}
|
);
|
||||||
notFoundContent={"Removed."}
|
}}
|
||||||
{...restProps}
|
notFoundContent={"Removed."}
|
||||||
>
|
{...restProps}
|
||||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
>
|
||||||
{t("billlines.labels.other")}
|
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||||
</Select.Option>
|
{t("billlines.labels.other")}
|
||||||
{options
|
</Select.Option>
|
||||||
? options.map((item) => (
|
{options
|
||||||
<Option
|
? options.map((item) => (
|
||||||
disabled={allowRemoved ? false : item.removed}
|
<Option
|
||||||
key={item.id}
|
disabled={allowRemoved ? false : item.removed}
|
||||||
value={item.id}
|
key={item.id}
|
||||||
cost={item.act_price ? item.act_price : 0}
|
value={item.id}
|
||||||
part_type={item.part_type}
|
cost={item.act_price ? item.act_price : 0}
|
||||||
line_desc={item.line_desc}
|
part_type={item.part_type}
|
||||||
part_qty={item.part_qty}
|
line_desc={item.line_desc}
|
||||||
oem_partno={item.oem_partno}
|
part_qty={item.part_qty}
|
||||||
alt_partno={item.alt_partno}
|
oem_partno={item.oem_partno}
|
||||||
act_price={item.act_price}
|
alt_partno={item.alt_partno}
|
||||||
style={{
|
act_price={item.act_price}
|
||||||
...(item.removed ? { textDecoration: "line-through" } : {}),
|
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>
|
||||||
{item.act_price === 0 && item.mod_lb_hrs > 0 && (
|
{item.act_price === 0 && item.mod_lb_hrs > 0 && (
|
||||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
<span style={{float: "right", paddingleft: "1rem"}}>
|
||||||
{`${item.mod_lb_hrs} units`}
|
{`${item.mod_lb_hrs} units`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
<span style={{float: "right", paddingleft: "1rem"}}>
|
||||||
{item.act_price
|
{item.act_price
|
||||||
? `$${item.act_price && item.act_price.toFixed(2)}`
|
? `$${item.act_price && item.act_price.toFixed(2)}`
|
||||||
: ``}
|
: ``}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default forwardRef(BillLineSearchSelect);
|
export default forwardRef(BillLineSearchSelect);
|
||||||
|
|||||||
@@ -1,101 +1,97 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import {gql, useMutation} from "@apollo/client";
|
||||||
import { Button, notification } from "antd";
|
import {Button, notification} from "antd";
|
||||||
import { gql } from "@apollo/client";
|
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 {createStructuredSelector} from "reselect";
|
||||||
|
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||||
|
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import {
|
|
||||||
selectAuthLevel,
|
|
||||||
selectBodyshop,
|
|
||||||
selectCurrentUser,
|
|
||||||
} from "../../redux/user/user.selectors";
|
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
|
||||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillMarkExportedButton);
|
)(BillMarkExportedButton);
|
||||||
|
|
||||||
export function BillMarkExportedButton({
|
export function BillMarkExportedButton({
|
||||||
currentUser,
|
currentUser,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
authLevel,
|
authLevel,
|
||||||
bill,
|
bill,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
|
|
||||||
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",
|
||||||
});
|
});
|
||||||
|
|
||||||
await insertExportLog({
|
if (hasAccess)
|
||||||
variables: {
|
return (
|
||||||
logs: [
|
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||||
{
|
{t("bills.labels.markexported")}
|
||||||
bodyshopid: bodyshop.id,
|
</Button>
|
||||||
billid: bill.id,
|
);
|
||||||
successful: true,
|
|
||||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.errors) {
|
return <></>;
|
||||||
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,82 +1,79 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import {gql, useMutation} from "@apollo/client";
|
||||||
import { Button, notification } from "antd";
|
import {Button, notification} from "antd";
|
||||||
import { gql } from "@apollo/client";
|
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 {createStructuredSelector} from "reselect";
|
||||||
|
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
|
||||||
|
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import {
|
|
||||||
selectAuthLevel,
|
|
||||||
selectBodyshop,
|
|
||||||
} from "../../redux/user/user.selectors";
|
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillMarkForReexportButton);
|
)(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 (!result.errors) {
|
if (hasAccess)
|
||||||
notification["success"]({
|
return (
|
||||||
message: t("bills.successes.reexport"),
|
<Button
|
||||||
});
|
loading={loading}
|
||||||
} else {
|
disabled={!bill.exported}
|
||||||
notification["error"]({
|
onClick={handleUpdate}
|
||||||
message: t("bills.errors.saving", {
|
>
|
||||||
error: JSON.stringify(result.errors),
|
{t("bills.labels.markforreexport")}
|
||||||
}),
|
</Button>
|
||||||
});
|
);
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
//Get the owner details, populate it all back into the job.
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAccess = HasRbacAccess({
|
return <></>;
|
||||||
bodyshop,
|
|
||||||
authLevel,
|
|
||||||
action: "bills:reexport",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasAccess)
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
loading={loading}
|
|
||||||
disabled={!bill.exported}
|
|
||||||
onClick={handleUpdate}
|
|
||||||
>
|
|
||||||
{t("bills.labels.markforreexport")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +1,151 @@
|
|||||||
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 moment from "moment";
|
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 {
|
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||||
selectBodyshop,
|
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
|
||||||
selectCurrentUser,
|
|
||||||
} from "../../redux/user/user.selectors";
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BilllineAddInventory);
|
)(BilllineAddInventory);
|
||||||
|
|
||||||
export function BilllineAddInventory({
|
export function BilllineAddInventory({
|
||||||
currentUser,
|
currentUser,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
billline,
|
billline,
|
||||||
disabled,
|
disabled,
|
||||||
jobid,
|
jobid,
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { billid } = queryString.parse(useLocation().search);
|
const {billid} = queryString.parse(useLocation().search);
|
||||||
|
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||||
|
|
||||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
const addToInventory = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const addToInventory = async () => {
|
//Check to make sure there are no existing items already in the inventory.
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
//Check to make sure there are no existing items already in the inventory.
|
const cm = {
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
invoice_number: "ih",
|
||||||
|
jobid: jobid,
|
||||||
|
isinhouse: true,
|
||||||
|
is_credit_memo: true,
|
||||||
|
date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||||
|
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||||
|
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||||
|
total: 0,
|
||||||
|
billlines: [
|
||||||
|
{
|
||||||
|
actual_price: billline.actual_price,
|
||||||
|
actual_cost: billline.actual_cost,
|
||||||
|
quantity: billline.quantity,
|
||||||
|
line_desc: billline.line_desc,
|
||||||
|
cost_center: billline.cost_center,
|
||||||
|
deductedfromlbr: billline.deductedfromlbr,
|
||||||
|
applicable_taxes: {
|
||||||
|
local: billline.applicable_taxes.local,
|
||||||
|
state: billline.applicable_taxes.state,
|
||||||
|
federal: billline.applicable_taxes.federal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const cm = {
|
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||||
vendorid: bodyshop.inhousevendorid,
|
|
||||||
invoice_number: "ih",
|
const insertResult = await insertInventoryLine({
|
||||||
jobid: jobid,
|
variables: {
|
||||||
isinhouse: true,
|
joblineId:
|
||||||
is_credit_memo: true,
|
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.
|
||||||
date: moment().format("YYYY-MM-DD"),
|
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
inv: {
|
||||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
shopid: bodyshop.id,
|
||||||
total: 0,
|
billlineid: billline.id,
|
||||||
billlines: [
|
actual_price: billline.actual_price,
|
||||||
{
|
actual_cost: billline.actual_cost,
|
||||||
actual_price: billline.actual_price,
|
quantity: billline.quantity,
|
||||||
actual_cost: billline.actual_cost,
|
line_desc: billline.line_desc,
|
||||||
quantity: billline.quantity,
|
},
|
||||||
line_desc: billline.line_desc,
|
cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
|
||||||
cost_center: billline.cost_center,
|
pol: {
|
||||||
deductedfromlbr: billline.deductedfromlbr,
|
returnfrombill: billid,
|
||||||
applicable_taxes: {
|
vendorid: bodyshop.inhousevendorid,
|
||||||
local: billline.applicable_taxes.local,
|
deliver_by: dayjs().format("YYYY-MM-DD"),
|
||||||
state: billline.applicable_taxes.state,
|
parts_order_lines: {
|
||||||
federal: billline.applicable_taxes.federal,
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
return (
|
||||||
|
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||||
const insertResult = await insertInventoryLine({
|
<Button
|
||||||
variables: {
|
loading={loading}
|
||||||
joblineId:
|
disabled={
|
||||||
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: moment().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,235 +1,236 @@
|
|||||||
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) =>
|
setBillEnterContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
dispatch(setModalContext({context: context, modal: "billEnter"})),
|
||||||
setReconciliationContext: (context) =>
|
setReconciliationContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "reconciliation" })),
|
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} />
|
|
||||||
<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>
|
||||||
<Button onClick={() => refetch()}>
|
{showView && (
|
||||||
<SyncOutlined />
|
<Button onClick={() => handleOnRowClick(record)}>
|
||||||
</Button>
|
<EditFilled/>
|
||||||
{job && job.converted ? (
|
</Button>
|
||||||
<>
|
)}
|
||||||
<Button
|
<BillDeleteButton bill={record}/>
|
||||||
onClick={() => {
|
<BillDetailEditReturnComponent
|
||||||
setBillEnterContext({
|
data={{bills_by_pk: {...record, jobid: job.id}}}
|
||||||
actions: { refetch: billsQuery.refetch },
|
disabled={
|
||||||
context: {
|
record.is_credit_memo ||
|
||||||
job,
|
record.vendorid === bodyshop.inhousevendorid ||
|
||||||
},
|
jobRO
|
||||||
});
|
}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
{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
|
{record.isinhouse && (
|
||||||
placeholder={t("general.labels.search")}
|
<PrintWrapperComponent
|
||||||
value={searchText}
|
templateObject={{
|
||||||
onChange={(e) => {
|
name: Templates.inhouse_invoice.key,
|
||||||
e.preventDefault();
|
variables: {id: record.id},
|
||||||
setSearchText(e.target.value);
|
}}
|
||||||
}}
|
messageObject={{subject: Templates.inhouse_invoice.subject}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
);
|
||||||
>
|
const columns = [
|
||||||
<Table
|
{
|
||||||
loading={billsQuery.loading}
|
title: t("bills.fields.vendorname"),
|
||||||
scroll={{
|
dataIndex: "vendorname",
|
||||||
x: true, // y: "50rem"
|
key: "vendorname",
|
||||||
}}
|
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||||
columns={columns}
|
sortOrder:
|
||||||
rowKey="id"
|
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||||
dataSource={filteredBills}
|
render: (text, record) => <span>{record.vendor.name}</span>,
|
||||||
onChange={handleTableChange}
|
},
|
||||||
/>
|
{
|
||||||
</Card>
|
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"
|
||||||
|
dataSource={filteredBills}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillsListTableComponent);
|
)(BillsListTableComponent);
|
||||||
|
|||||||
@@ -1,121 +1,121 @@
|
|||||||
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 { useHistory, useLocation } from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import { Table, Input } 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 = useHistory();
|
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],
|
{
|
||||||
type: "radio",
|
title: t("vendors.fields.cost_center"),
|
||||||
}}
|
dataIndex: "cost_center",
|
||||||
onRow={(record, rowIndex) => {
|
key: "cost_center",
|
||||||
return {
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
onClick: (event) => {
|
sortOrder:
|
||||||
handleOnRowClick(record);
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
}, // 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,54 +1,64 @@
|
|||||||
import { HomeFilled } from "@ant-design/icons";
|
import {HomeFilled} from "@ant-design/icons";
|
||||||
import { Breadcrumb, Row, Col } 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 { useTreatments } 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 { OpenSearch } = useTreatments(
|
|
||||||
["OpenSearch"],
|
|
||||||
{},
|
|
||||||
bodyshop && bodyshop.imexshopid
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const {treatments: {OpenSearch}} = useSplitTreatments({
|
||||||
<Row className="breadcrumb-container">
|
attributes: {},
|
||||||
<Col xs={24} sm={24} md={16}>
|
names: ["OpenSearch"],
|
||||||
<Breadcrumb separator=">">
|
splitKey: bodyshop && bodyshop.imexshopid,
|
||||||
<Breadcrumb.Item>
|
});
|
||||||
<Link to={`/manage`}>
|
// TODO - Client Update - Technically key is not doing anything here
|
||||||
<HomeFilled />{" "}
|
return (
|
||||||
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
<Row className="breadcrumb-container">
|
||||||
""}
|
<Col xs={24} sm={24} md={16}>
|
||||||
</Link>
|
<Breadcrumb
|
||||||
</Breadcrumb.Item>
|
separator=">"
|
||||||
{breadcrumbs.map((item) =>
|
items={[
|
||||||
item.link ? (
|
{
|
||||||
<Breadcrumb.Item key={item.label}>
|
key: "home",
|
||||||
<Link to={item.link}>{item.label} </Link>
|
title: (
|
||||||
</Breadcrumb.Item>
|
<Link to={`/manage/`}>
|
||||||
) : (
|
<HomeFilled/>{" "}
|
||||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
||||||
)
|
""}
|
||||||
)}
|
</Link>
|
||||||
</Breadcrumb>
|
),
|
||||||
</Col>
|
},
|
||||||
<Col xs={24} sm={24} md={8}>
|
...breadcrumbs.map((item) =>
|
||||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
|
item.link
|
||||||
</Col>
|
? {
|
||||||
</Row>
|
key: item.label,
|
||||||
);
|
title: <Link to={item.link}>{item.label}</Link>,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: item.label,
|
||||||
|
title: item.label,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={8}>
|
||||||
|
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
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,
|
caBcEtfTableModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const { visible } = 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"
|
values.sendby === "email" ? "e" : "p"
|
||||||
|
);
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}, [visible, form]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ContractsFindModalContainer);
|
)(ContractsFindModalContainer);
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
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")}
|
label={t("general.labels.sendby")}
|
||||||
name="sendby"
|
name="sendby"
|
||||||
initialValue="print"
|
initialValue="print"
|
||||||
>
|
>
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
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 }) {
|
|
||||||
const [visibility, setVisibility] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
export default function CABCpvrtCalculator({disabled, form}) {
|
||||||
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const {t} = useTranslation();
|
||||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
|
||||||
form.setFieldsValue({
|
|
||||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
|
||||||
});
|
|
||||||
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
|
||||||
setVisibility(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const popContent = (
|
const handleFinish = async (values) => {
|
||||||
<div>
|
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||||
<Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}>
|
form.setFieldsValue({
|
||||||
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
||||||
<InputNumber precision={2} min={0} />
|
});
|
||||||
</Form.Item>
|
form.setFields([{name: "ca_bc_pvrt", touched: true}]);
|
||||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
setVisibility(false);
|
||||||
<InputNumber precision={0} min={0} />
|
};
|
||||||
</Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
{t("general.actions.calculate")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const popContent = (
|
||||||
<Popover
|
<div>
|
||||||
destroyTooltipOnHide
|
<Form onFinish={handleFinish} initialValues={{rate: 1.5}}>
|
||||||
content={popContent}
|
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
||||||
visible={visibility}
|
<InputNumber precision={2} min={0}/>
|
||||||
disabled={disabled}
|
</Form.Item>
|
||||||
>
|
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<InputNumber precision={0} min={0}/>
|
||||||
<CalculatorFilled />
|
</Form.Item>
|
||||||
</Button>
|
<Button type="primary" htmlType="submit">
|
||||||
</Popover>
|
{t("general.actions.calculate")}
|
||||||
);
|
</Button>
|
||||||
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
destroyTooltipOnHide
|
||||||
|
content={popContent}
|
||||||
|
open={visibility}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
|
<CalculatorFilled/>
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,374 +1,360 @@
|
|||||||
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 {
|
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd";
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Spin,
|
|
||||||
Statistic,
|
|
||||||
notification,
|
|
||||||
} from "antd";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
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 {
|
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries";
|
||||||
INSERT_PAYMENT_RESPONSE,
|
import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries";
|
||||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||||
} from "../../graphql/payment_response.queries";
|
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
import {selectCardPayment} from "../../redux/modals/modals.selectors";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|
||||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import 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 }) =>
|
insertAuditTrail: ({jobid, operation}) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({jobid, operation})),
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CardPaymentModalComponent = ({
|
const CardPaymentModalComponent = ({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
cardPaymentModal,
|
cardPaymentModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
insertAuditTrail,
|
insertAuditTrail,
|
||||||
}) => {
|
}) => {
|
||||||
const { context } = cardPaymentModal;
|
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] },
|
variables: {jobids: [context.jobid]},
|
||||||
skip: true,
|
skip: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||||
//Initialize the intellipay window.
|
//Initialize the intellipay window.
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
window.intellipay.runOnClose(() => {
|
window.intellipay.runOnClose(() => {
|
||||||
//window.intellipay.initialize();
|
//window.intellipay.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
window.intellipay.runOnApproval(async function (response) {
|
||||||
console.warn("*** Running On Approval Script ***");
|
console.warn("*** Running On Approval Script ***");
|
||||||
form.setFieldValue("paymentResponse", response);
|
form.setFieldValue("paymentResponse", response);
|
||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
window.intellipay.runOnNonApproval(async function (response) {
|
||||||
// Mutate unsuccessful payment
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
const { payments } = form.getFieldsValue();
|
const {payments} = form.getFieldsValue();
|
||||||
|
|
||||||
await insertPaymentResponse({
|
await insertPaymentResponse({
|
||||||
variables: {
|
variables: {
|
||||||
paymentResponse: payments.map((payment) => ({
|
paymentResponse: payments.map((payment) => ({
|
||||||
amount: payment.amount,
|
amount: payment.amount,
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
jobid: payment.jobid,
|
jobid: payment.jobid,
|
||||||
declinereason: response.declinereason,
|
declinereason: response.declinereason,
|
||||||
ext_paymentid: response.paymentid.toString(),
|
ext_paymentid: response.paymentid.toString(),
|
||||||
successful: false,
|
successful: false,
|
||||||
response,
|
response,
|
||||||
})),
|
})),
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
payments.forEach((payment) =>
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: payment.jobid,
|
|
||||||
operation: AuditTrailMapping.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: moment(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 () => {
|
payments.forEach((payment) =>
|
||||||
setLoading(true);
|
insertAuditTrail({
|
||||||
|
jobid: payment.jobid,
|
||||||
|
operation: AuditTrailMapping.failedpayment(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//Validate
|
const handleFinish = async (values) => {
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await insertPayment({
|
||||||
} catch (error) {
|
variables: {
|
||||||
setLoading(false);
|
paymentInput: values.payments.map((payment) => ({
|
||||||
return;
|
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,
|
||||||
|
|
||||||
try {
|
jobid: payment.jobid,
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
declinereason: values.paymentResponse.declinereason,
|
||||||
bodyshop,
|
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||||
refresh: !!window.intellipay,
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (window.intellipay) {
|
const handleIntelliPayCharge = async () => {
|
||||||
// eslint-disable-next-line no-eval
|
setLoading(true);
|
||||||
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 (
|
//Validate
|
||||||
<Card title="Card Payment">
|
try {
|
||||||
<Spin spinning={loading}>
|
await form.validateFields();
|
||||||
<Form
|
} catch (error) {
|
||||||
onFinish={handleFinish}
|
setLoading(false);
|
||||||
form={form}
|
return;
|
||||||
layout="vertical"
|
}
|
||||||
initialValues={{
|
|
||||||
payments: context.jobid ? [{ jobid: context.jobid }] : [],
|
try {
|
||||||
}}
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
>
|
bodyshop,
|
||||||
<Form.List name={["payments"]}>
|
refresh: !!window.intellipay,
|
||||||
{(fields, { add, remove, move }) => {
|
});
|
||||||
return (
|
|
||||||
<div>
|
if (window.intellipay) {
|
||||||
{fields.map((field, index) => (
|
// eslint-disable-next-line no-eval
|
||||||
<Form.Item key={field.key}>
|
eval(response.data);
|
||||||
<Row gutter={[16, 16]}>
|
SetIntellipayCallbackFunctions();
|
||||||
<Col span={16}>
|
window.intellipay.autoOpen();
|
||||||
<Form.Item
|
} else {
|
||||||
key={`${index}jobid`}
|
var rg = document.createRange();
|
||||||
label={t("jobs.fields.ro_number")}
|
let node = rg.createContextualFragment(response.data);
|
||||||
name={[field.name, "jobid"]}
|
document.documentElement.appendChild(node);
|
||||||
rules={[
|
SetIntellipayCallbackFunctions();
|
||||||
{
|
window.intellipay.isAutoOpen = true;
|
||||||
required: true,
|
window.intellipay.initialize();
|
||||||
//message: t("general.validation.required"),
|
}
|
||||||
},
|
} catch (error) {
|
||||||
]}
|
notification.open({
|
||||||
>
|
type: "error",
|
||||||
<JobSearchSelectComponent
|
message: t("job_payments.notifications.error.openingip"),
|
||||||
notExported={false}
|
});
|
||||||
clm_no
|
setLoading(false);
|
||||||
/>
|
}
|
||||||
</Form.Item>
|
};
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
return (
|
||||||
<Form.Item
|
<Card title="Card Payment">
|
||||||
key={`${index}amount`}
|
<Spin spinning={loading}>
|
||||||
label={t("payments.fields.amount")}
|
<Form
|
||||||
name={[field.name, "amount"]}
|
onFinish={handleFinish}
|
||||||
rules={[
|
form={form}
|
||||||
{
|
layout="vertical"
|
||||||
required: true,
|
initialValues={{
|
||||||
//message: t("general.validation.required"),
|
payments: context.jobid ? [{jobid: context.jobid}] : [],
|
||||||
},
|
}}
|
||||||
]}
|
>
|
||||||
>
|
<Form.List name={["payments"]}>
|
||||||
<CurrencyFormItemComponent />
|
{(fields, {add, remove, move}) => {
|
||||||
</Form.Item>
|
return (
|
||||||
</Col>
|
<div>
|
||||||
<Col span={2}>
|
{fields.map((field, index) => (
|
||||||
<DeleteFilled
|
<Form.Item key={field.key}>
|
||||||
style={{ margin: "1rem" }}
|
<Row gutter={[16, 16]}>
|
||||||
onClick={() => {
|
<Col span={16}>
|
||||||
remove(field.name);
|
<Form.Item
|
||||||
}}
|
key={`${index}jobid`}
|
||||||
/>
|
label={t("jobs.fields.ro_number")}
|
||||||
</Col>
|
name={[field.name, "jobid"]}
|
||||||
</Row>
|
rules={[
|
||||||
</Form.Item>
|
{
|
||||||
))}
|
required: true,
|
||||||
<Form.Item>
|
//message: t("general.validation.required"),
|
||||||
<Button
|
},
|
||||||
type="dashed"
|
]}
|
||||||
onClick={() => {
|
>
|
||||||
add();
|
<JobSearchSelectComponent
|
||||||
}}
|
notExported={false}
|
||||||
style={{ width: "100%" }}
|
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
|
||||||
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
|
prevValues.payments?.map((p) => p?.jobid).join() !==
|
||||||
|
curValues.payments?.map((p) => p?.jobid).join()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("general.actions.add")}
|
{() => {
|
||||||
</Button>
|
console.log("Updating the owner info section.");
|
||||||
</Form.Item>
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||||
</div>
|
const {payments} = form.getFieldsValue();
|
||||||
);
|
if (
|
||||||
}}
|
payments?.length > 0 &&
|
||||||
</Form.List>
|
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||||
|
) {
|
||||||
|
console.log("**Calling refetch.");
|
||||||
|
refetch({jobids: payments.map((p) => p.jobid)});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"Acc info",
|
||||||
|
data,
|
||||||
|
payments && data && data.jobs.length > 0
|
||||||
|
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="account"
|
||||||
|
//type="hidden"
|
||||||
|
value={
|
||||||
|
payments && data && data.jobs.length > 0
|
||||||
|
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</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);
|
||||||
|
|
||||||
<Form.Item
|
return (
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
<Space style={{float: "right"}}>
|
||||||
prevValues.payments?.map((p) => p?.jobid).join() !==
|
<Statistic
|
||||||
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 (
|
//type="hidden"
|
||||||
payments?.length > 0 &&
|
value={totalAmountToCharge?.toFixed(2)}
|
||||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
hidden
|
||||||
) {
|
/>
|
||||||
console.log("**Calling refetch.");
|
<Button
|
||||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
type="primary"
|
||||||
}
|
// data-ipayname="submit"
|
||||||
console.log(
|
className="ipayfield"
|
||||||
"Acc info",
|
loading={queryLoading || loading}
|
||||||
data,
|
disabled={!(totalAmountToCharge > 0)}
|
||||||
payments && data && data.jobs.length > 0
|
onClick={handleIntelliPayCharge}
|
||||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
>
|
||||||
: null
|
{t("job_payments.buttons.proceedtopayment")}
|
||||||
);
|
</Button>
|
||||||
return (
|
</Space>
|
||||||
<>
|
);
|
||||||
<Input
|
}}
|
||||||
className="ipayfield"
|
</Form.Item>
|
||||||
data-ipayname="account"
|
|
||||||
//type="hidden"
|
|
||||||
value={
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<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
|
|
||||||
}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</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 (
|
{/* Lightbox payment response when it is completed */}
|
||||||
<Space style={{ float: "right" }}>
|
<Form.Item name="paymentResponse" hidden>
|
||||||
<Statistic
|
<Input type="hidden"/>
|
||||||
title="Amount To Charge"
|
</Form.Item>
|
||||||
value={totalAmountToCharge}
|
</Form>
|
||||||
precision={2}
|
</Spin>
|
||||||
/>
|
</Card>
|
||||||
<Input
|
);
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="amount"
|
|
||||||
//type="hidden"
|
|
||||||
value={totalAmountToCharge?.toFixed(2)}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(CardPaymentModalComponent);
|
)(CardPaymentModalComponent);
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
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,
|
cardPaymentModal,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
}) {
|
}) {
|
||||||
const { visible } = cardPaymentModal;
|
const {open} = cardPaymentModal;
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOK = () => {
|
const handleOK = () => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={visible}
|
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(CardPaymentModalContainer);
|
)(CardPaymentModalContainer);
|
||||||
|
|||||||
@@ -1,106 +1,98 @@
|
|||||||
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 { connect } from "react-redux";
|
import {messaging, requestForToken} from "../../firebase/firebase.utils";
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
|
||||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
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";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
export function ChatAffixContainer({bodyshop, chatVisible}) {
|
||||||
bodyshop: selectBodyshop,
|
const {t} = useTranslation();
|
||||||
chatVisible: selectChatVisible,
|
const client = useApolloClient();
|
||||||
});
|
useEffect(() => {
|
||||||
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
async function SubscribeToTopic() {
|
||||||
const { t } = useTranslation();
|
try {
|
||||||
const client = useApolloClient();
|
const r = await axios.post("/notifications/subscribe", {
|
||||||
useEffect(() => {
|
fcm_tokens: await getToken(messaging, {
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
vapidKey: process.env.REACT_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({
|
||||||
|
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>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function SubscribeToTopic() {
|
SubscribeToTopic();
|
||||||
try {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
}, [bodyshop]);
|
||||||
fcm_tokens: await getToken(messaging, {
|
|
||||||
vapidKey: process.env.REACT_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({
|
|
||||||
type: "warning",
|
|
||||||
message: t("general.errors.fcm"),
|
|
||||||
btn: (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await requestForToken();
|
|
||||||
|
|
||||||
SubscribeToTopic();
|
useEffect(() => {
|
||||||
}}
|
function handleMessage(payload) {
|
||||||
>
|
FcmHandler({
|
||||||
{t("general.actions.tryagain")}
|
client,
|
||||||
</Button>
|
payload: (payload && payload.data && payload.data.data) || payload.data,
|
||||||
<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();
|
let stopMessageListener, channel;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
try {
|
||||||
}, [bodyshop]);
|
stopMessageListener = onMessage(messaging, handleMessage);
|
||||||
|
channel = new BroadcastChannel("imex-sw-messages");
|
||||||
|
channel.addEventListener("message", handleMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Unable to set event listeners.");
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
stopMessageListener && stopMessageListener();
|
||||||
|
channel && channel.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
function handleMessage(payload) {
|
|
||||||
FcmHandler({
|
|
||||||
client,
|
|
||||||
payload: (payload && payload.data && payload.data.data) || payload.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let stopMessageListenr, channel;
|
|
||||||
try {
|
|
||||||
stopMessageListenr = onMessage(messaging, handleMessage);
|
|
||||||
channel = new BroadcastChannel("imex-sw-messages");
|
|
||||||
channel.addEventListener("message", handleMessage);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Unable to set event listeners.");
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
stopMessageListenr && stopMessageListenr();
|
|
||||||
channel && channel.removeEventListener("message", handleMessage);
|
|
||||||
};
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
return (
|
||||||
|
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
return (
|
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
|
||||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
</div>
|
||||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
|
||||||
|
export default ChatAffixContainer;
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
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.unarchive")
|
||||||
: t("messaging.labels.archive")}
|
: t("messaging.labels.archive")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +1,122 @@
|
|||||||
import { Badge, List, 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 {
|
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
|
||||||
AutoSizer,
|
import {createStructuredSelector} from "reselect";
|
||||||
CellMeasurer,
|
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
|
||||||
CellMeasurerCache,
|
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
||||||
List as VirtualizedList,
|
import {TimeAgoFormatter} from "../../utils/DateFormatter";
|
||||||
} from "react-virtualized";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
|
||||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
||||||
|
import _ from "lodash";
|
||||||
import "./chat-conversation-list.styles.scss";
|
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 =
|
||||||
|
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
|
const cardContentLeft = item.job_conversations.length > 0
|
||||||
|
? item.job_conversations.map((j, idx) => (
|
||||||
|
<Tag key={idx}>{j.job.ro_number}</Tag>
|
||||||
|
))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
|
||||||
|
OwnerNameDisplayFunction(j.job)
|
||||||
|
))}</>
|
||||||
|
|
||||||
|
const cardTitle = <>
|
||||||
|
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||||
|
{item.job_conversations.length > 0 ? (
|
||||||
|
<Space direction="vertical">
|
||||||
|
{names}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space>
|
||||||
|
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
|
||||||
|
|
||||||
|
const getCardStyle = () =>
|
||||||
|
item.id === selectedConversation
|
||||||
|
? {backgroundColor: 'rgba(128, 128, 128, 0.2)'}
|
||||||
|
: {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'};
|
||||||
|
|
||||||
|
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 (
|
||||||
<CellMeasurer
|
<div className="chat-list-container">
|
||||||
key={key}
|
<AutoSizer>
|
||||||
cache={cache}
|
{({height, width}) => (
|
||||||
parent={parent}
|
<VirtualizedList
|
||||||
columnIndex={0}
|
height={height}
|
||||||
rowIndex={index}
|
width={width}
|
||||||
>
|
rowCount={conversationList.length}
|
||||||
<List.Item
|
rowHeight={cache.rowHeight}
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
rowRenderer={rowRenderer}
|
||||||
className={`chat-list-item ${
|
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
|
||||||
item.id === selectedConversation
|
if (scrollTop + clientHeight === scrollHeight) {
|
||||||
? "chat-list-selected-conversation"
|
loadMoreConversations();
|
||||||
: null
|
}
|
||||||
}`}
|
}}
|
||||||
style={style}
|
/>
|
||||||
>
|
)}
|
||||||
<div
|
</AutoSizer>
|
||||||
style={{
|
</div>
|
||||||
display: "inline-block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label && <div className="chat-name">{item.label}</div>}
|
|
||||||
{item.job_conversations.length > 0 ? (
|
|
||||||
<div className="chat-name">
|
|
||||||
{item.job_conversations.map((j, idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
<OwnerNameDisplay ownerObject={j.job} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "inline-block" }}>
|
|
||||||
<div>
|
|
||||||
{item.job_conversations.length > 0
|
|
||||||
? item.job_conversations.map((j, idx) => (
|
|
||||||
<Tag key={idx} className="ro-number-tag">
|
|
||||||
{j.job.ro_number}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
|
||||||
</div>
|
|
||||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ChatConversationListComponent);
|
)(ChatConversationListComponent);
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
.chat-list-selected-conversation {
|
|
||||||
background-color: rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
display: flex;
|
.ant-card-head {
|
||||||
flex-direction: row;
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #ff7a00;
|
color: #ff7a00;
|
||||||
}
|
}
|
||||||
.chat-name {
|
|
||||||
flex: 1;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.ro-number-tag {
|
|
||||||
align-self: baseline;
|
|
||||||
}
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-bottom: 1px solid gainsboro;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", {
|
logImEXEvent("messaging_remove_job_tag", {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId,
|
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,21 @@ 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}
|
{conversation && conversation.phone_num}
|
||||||
</PhoneNumberFormatter>
|
</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation}/>
|
||||||
<ChatPrintButton conversation={conversation} />
|
<ChatPrintButton conversation={conversation}/>
|
||||||
<ChatConversationTitleTags
|
<ChatConversationTitleTags
|
||||||
jobConversations={
|
jobConversations={
|
||||||
(conversation && conversation.job_conversations) || []
|
(conversation && conversation.job_conversations) || []
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ChatTagRoContainer conversation={conversation || []} />
|
<ChatTagRoContainer conversation={conversation || []}/>
|
||||||
<ChatArchiveButton conversation={conversation} />
|
<ChatArchiveButton conversation={conversation}/>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ 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,
|
subState,
|
||||||
conversation,
|
conversation,
|
||||||
messages,
|
messages,
|
||||||
handleMarkConversationAsRead,
|
handleMarkConversationAsRead,
|
||||||
}) {
|
}) {
|
||||||
const [loading, error] = subState;
|
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,89 +1,87 @@
|
|||||||
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 {
|
import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries";
|
||||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
import {MARK_MESSAGES_AS_READ_BY_CONVERSATION} from "../../graphql/messages.queries";
|
||||||
GET_CONVERSATION_DETAILS,
|
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
||||||
} from "../../graphql/conversations.queries";
|
|
||||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
import 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 [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
const [markConversationRead] = useMutation(
|
const [markConversationRead] = useMutation(
|
||||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||||
{
|
{
|
||||||
variables: { conversationId: selectedConversation },
|
variables: {conversationId: selectedConversation},
|
||||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: cache.identify({
|
id: cache.identify({
|
||||||
__typename: "conversations",
|
__typename: "conversations",
|
||||||
id: selectedConversation,
|
id: selectedConversation,
|
||||||
}),
|
}),
|
||||||
fields: {
|
fields: {
|
||||||
messages_aggregate(cached) {
|
messages_aggregate(cached) {
|
||||||
return { aggregate: { count: 0 } };
|
return {aggregate: {count: 0}};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const unreadCount =
|
const unreadCount =
|
||||||
data &&
|
data &&
|
||||||
data.messages &&
|
data.messages &&
|
||||||
data.messages.reduce((acc, val) => {
|
data.messages.reduce((acc, val) => {
|
||||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const handleMarkConversationAsRead = async () => {
|
const handleMarkConversationAsRead = async () => {
|
||||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||||
setMarkingAsReadInProgress(true);
|
setMarkingAsReadInProgress(true);
|
||||||
await markConversationRead({});
|
await markConversationRead({});
|
||||||
await axios.post("/sms/markConversationRead", {
|
await axios.post("/sms/markConversationRead", {
|
||||||
conversationid: selectedConversation,
|
conversationid: selectedConversation,
|
||||||
imexshopid: bodyshop.imexshopid,
|
imexshopid: bodyshop.imexshopid,
|
||||||
});
|
});
|
||||||
setMarkingAsReadInProgress(false);
|
setMarkingAsReadInProgress(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatConversationComponent
|
<ChatConversationComponent
|
||||||
subState={[loading || convoLoading, error || convoError]}
|
subState={[loading || convoLoading, error || convoError]}
|
||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||||
messages={data ? data.messages : []}
|
messages={data ? data.messages : []}
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,68 @@
|
|||||||
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 }) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
const [value, setValue] = useState(conversation.label);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
export default function ChatLabel({conversation}) {
|
||||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [value, setValue] = useState(conversation.label);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const {t} = useTranslation();
|
||||||
setLoading(true);
|
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||||
try {
|
|
||||||
const response = await updateLabel({
|
const handleSave = async () => {
|
||||||
variables: { id: conversation.id, label: value },
|
setLoading(true);
|
||||||
});
|
try {
|
||||||
if (response.errors) {
|
const response = await updateLabel({
|
||||||
notification["error"]({
|
variables: {id: conversation.id, label: value},
|
||||||
message: t("messages.errors.updatinglabel", {
|
});
|
||||||
error: JSON.stringify(response.errors),
|
if (response.errors) {
|
||||||
}),
|
notification["error"]({
|
||||||
});
|
message: t("messages.errors.updatinglabel", {
|
||||||
} else {
|
error: JSON.stringify(response.errors),
|
||||||
setEditing(false);
|
}),
|
||||||
}
|
});
|
||||||
} catch (error) {
|
} else {
|
||||||
notification["error"]({
|
setEditing(false);
|
||||||
message: t("messages.errors.updatinglabel", {
|
}
|
||||||
error: JSON.stringify(error),
|
} catch (error) {
|
||||||
}),
|
notification["error"]({
|
||||||
});
|
message: t("messages.errors.updatinglabel", {
|
||||||
} finally {
|
error: JSON.stringify(error),
|
||||||
setLoading(false);
|
}),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
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,99 +1,100 @@
|
|||||||
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 from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobDocumentsLocalGalleryExternal
|
||||||
|
from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
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,
|
bodyshop,
|
||||||
selectedMedia,
|
selectedMedia,
|
||||||
setSelectedMedia,
|
setSelectedMedia,
|
||||||
conversation,
|
conversation,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
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] &&
|
||||||
conversation.job_conversations[0].jobid,
|
conversation.job_conversations[0].jobid,
|
||||||
},
|
},
|
||||||
|
|
||||||
skip:
|
skip:
|
||||||
!visible ||
|
!open ||
|
||||||
!conversation.job_conversations ||
|
!conversation.job_conversations ||
|
||||||
conversation.job_conversations.length === 0,
|
conversation.job_conversations.length === 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVisibleChange = (visible) => {
|
const handleVisibleChange = (change) => {
|
||||||
setVisible(visible);
|
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 && visible && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={
|
jobId={
|
||||||
conversation.job_conversations[0] &&
|
conversation.job_conversations[0] &&
|
||||||
conversation.job_conversations[0].jobid
|
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>
|
<div>{t("messaging.errors.noattachedjobs")}</div>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={t("messaging.labels.selectmedia")}
|
title={t("messaging.labels.selectmedia")}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
visible={visible}
|
open={open}
|
||||||
onVisibleChange={handleVisibleChange}
|
onOpenChange={handleVisibleChange}
|
||||||
>
|
>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
<PictureFilled style={{margin: "0 .5rem"}}/>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +1,113 @@
|
|||||||
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 moment from "moment";
|
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 {
|
import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized";
|
||||||
AutoSizer,
|
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||||
CellMeasurer,
|
|
||||||
CellMeasurerCache,
|
|
||||||
List,
|
|
||||||
} from "react-virtualized";
|
|
||||||
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 (
|
||||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
<div className="chat">
|
||||||
{({ measure, registerChild }) => (
|
<AutoSizer>
|
||||||
<div
|
{({height, width}) => (
|
||||||
ref={registerChild}
|
<List
|
||||||
onLoad={measure}
|
ref={virtualizedListRef}
|
||||||
style={style}
|
width={width}
|
||||||
className={`${
|
height={height}
|
||||||
messages[index].isoutbound ? "mine messages" : "yours messages"
|
rowHeight={_cache.rowHeight}
|
||||||
}`}
|
rowRenderer={_rowRenderer}
|
||||||
>
|
rowCount={messages.length}
|
||||||
<div className="message msgmargin">
|
overscanRowCount={10}
|
||||||
{MessageRender(messages[index])}
|
estimatedRowSize={150}
|
||||||
{StatusRender(messages[index].status)}
|
scrollToIndex={messages.length}
|
||||||
</div>
|
/>
|
||||||
{messages[index].isoutbound && (
|
)}
|
||||||
<div style={{ fontSize: 10 }}>
|
</AutoSizer>
|
||||||
{i18n.t("messaging.labels.sentby", {
|
</div>
|
||||||
by: messages[index].userid,
|
|
||||||
time: moment(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}
|
key={idx}
|
||||||
style={{ display: "flex", justifyContent: "center" }}
|
style={{display: "flex", justifyContent: "center"}}
|
||||||
>
|
>
|
||||||
<a href={i} target="__blank">
|
<a href={i} target="__blank">
|
||||||
<img alt="Received" className="message-img" src={i} />
|
<img alt="Received" className="message-img" src={i}/>
|
||||||
</a>
|
</a>
|
||||||
|
</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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
.yours {
|
.yours {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msgmargin {
|
.msgmargin {
|
||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
margin-bottom: 0.1rem;
|
margin-bottom: 0.1rem;
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
background: #eee;
|
background: #eee;
|
||||||
border-bottom-right-radius: 15px;
|
border-bottom-right-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message.last:after {
|
.yours .message.last:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,57 +1,55 @@
|
|||||||
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, {
|
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
|
||||||
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 }) =>
|
({getFieldValue}) =>
|
||||||
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
|
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<PhoneFormItem />
|
<PhoneFormItem/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
{t("messaging.actions.new")}
|
{t("messaging.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover trigger="click" content={popContent}>
|
<Popover trigger="click" content={popContent}>
|
||||||
<PlusCircleFilled />
|
<PlusCircleFilled/>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ChatNewConversation);
|
)(ChatNewConversation);
|
||||||
|
|||||||
@@ -1,52 +1,54 @@
|
|||||||
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,
|
bodyshop,
|
||||||
searchingForConversation,
|
searchingForConversation,
|
||||||
phone,
|
phone,
|
||||||
jobid,
|
jobid,
|
||||||
openChatByPhone,
|
openChatByPhone,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
if (!bodyshop.messagingservicesid)
|
if (!bodyshop.messagingservicesid)
|
||||||
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
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,24 +1,13 @@
|
|||||||
import {
|
import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons";
|
||||||
InfoCircleOutlined,
|
import {useLazyQuery, useQuery} from "@apollo/client";
|
||||||
MessageOutlined,
|
import {Badge, Card, Col, Row, Space, Tag, Tooltip, Typography} from "antd";
|
||||||
ShrinkOutlined,
|
import React, {useCallback, useEffect, useState} from "react";
|
||||||
SyncOutlined,
|
import {useTranslation} from "react-i18next";
|
||||||
} from "@ant-design/icons";
|
import {connect} from "react-redux";
|
||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import {createStructuredSelector} from "reselect";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import {toggleChatVisible} from "../../redux/messaging/messaging.actions";
|
||||||
import { useTranslation } from "react-i18next";
|
import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import {
|
|
||||||
CONVERSATION_LIST_QUERY,
|
|
||||||
UNREAD_CONVERSATION_COUNT,
|
|
||||||
} from "../../graphql/conversations.queries";
|
|
||||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
|
||||||
import {
|
|
||||||
selectChatVisible,
|
|
||||||
selectSelectedConversation,
|
|
||||||
} from "../../redux/messaging/messaging.selectors";
|
|
||||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
import 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";
|
||||||
@@ -26,118 +15,119 @@ 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,
|
chatVisible,
|
||||||
selectedConversation,
|
selectedConversation,
|
||||||
toggleChatVisible,
|
toggleChatVisible,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
const [pollInterval, setpollInterval] = useState(0);
|
||||||
|
|
||||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
...(pollInterval > 0 ? {pollInterval} : {}),
|
||||||
});
|
|
||||||
|
|
||||||
const [getConversations, { loading, data, refetch, fetchMore }] =
|
|
||||||
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
|
||||||
fetchPolicy: "network-only",
|
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
skip: !chatVisible,
|
|
||||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
const [getConversations, {loading, data, refetch, fetchMore}] =
|
||||||
|
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: !chatVisible,
|
||||||
|
...(pollInterval > 0 ? {pollInterval} : {}),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||||
if (fcmToken) {
|
|
||||||
setpollInterval(0);
|
|
||||||
} else {
|
|
||||||
setpollInterval(60000);
|
|
||||||
}
|
|
||||||
}, [fcmToken]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatVisible)
|
if (fcmToken) {
|
||||||
getConversations({
|
setpollInterval(0);
|
||||||
variables: {
|
} else {
|
||||||
offset: 0,
|
setpollInterval(60000);
|
||||||
},
|
}
|
||||||
});
|
}, [fcmToken]);
|
||||||
}, [chatVisible, getConversations]);
|
|
||||||
|
|
||||||
const loadMoreConversations = useCallback(() => {
|
useEffect(() => {
|
||||||
if (data)
|
if (chatVisible)
|
||||||
fetchMore({
|
getConversations({
|
||||||
variables: {
|
variables: {
|
||||||
offset: data.conversations.length,
|
offset: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [data, fetchMore]);
|
}, [chatVisible, getConversations]);
|
||||||
|
|
||||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
const loadMoreConversations = useCallback(() => {
|
||||||
|
if (data)
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
offset: data.conversations.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [data, fetchMore]);
|
||||||
|
|
||||||
return (
|
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||||
<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" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
return (
|
||||||
<Col span={8}>
|
<Badge count={unreadCount}>
|
||||||
{loading ? (
|
<Card size="small">
|
||||||
<LoadingSpinner />
|
{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"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<ChatConversationListComponent
|
<div
|
||||||
conversationList={data ? data.conversations : []}
|
onClick={() => toggleChatVisible()}
|
||||||
loadMoreConversations={loadMoreConversations}
|
style={{cursor: "pointer"}}
|
||||||
/>
|
>
|
||||||
|
<MessageOutlined className="chat-popup-info-icon"/>
|
||||||
|
<strong>{t("messaging.labels.messaging")}</strong>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Card>
|
||||||
<Col span={16}>
|
</Badge>
|
||||||
{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);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-popup-info-icon {
|
.chat-popup-info-icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
import {PlusCircleOutlined} from "@ant-design/icons";
|
||||||
import { Dropdown, Menu } 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 menu = (
|
|
||||||
<Menu>
|
|
||||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
|
||||||
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
|
|
||||||
{i.label}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||||
<div className={className}>
|
key: idx,
|
||||||
<Dropdown trigger={["click"]} overlay={menu}>
|
label: (i.label),
|
||||||
<PlusCircleOutlined />
|
onClick: () => setMessage(i.text),
|
||||||
</Dropdown>
|
}));
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Dropdown trigger={["click"]} menu={{items}}>
|
||||||
|
<PlusCircleOutlined/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ChatPresetsComponent);
|
)(ChatPresetsComponent);
|
||||||
|
|||||||
@@ -1,59 +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);
|
||||||
|
|
||||||
return (
|
const generateDocument = (type) => {
|
||||||
<Space wrap>
|
setLoading(true);
|
||||||
<PrinterOutlined
|
GenerateDocument(
|
||||||
onClick={() => {
|
|
||||||
setLoading(true);
|
|
||||||
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,
|
||||||
},
|
},
|
||||||
"p",
|
type,
|
||||||
conversation.id
|
conversation.id
|
||||||
);
|
).catch(e => {
|
||||||
setLoading(false);
|
console.warn('Something went wrong generating a document.');
|
||||||
}}
|
});
|
||||||
/>
|
setLoading(false);
|
||||||
<MailOutlined
|
}
|
||||||
onClick={() => {
|
|
||||||
setLoading(true);
|
return (
|
||||||
GenerateDocument(
|
<Space wrap>
|
||||||
{
|
<PrinterOutlined onClick={() => generateDocument('p')}/>
|
||||||
name: TemplateList("messaging").conversation_list.key,
|
<MailOutlined onClick={() => generateDocument('e')}/>
|
||||||
variables: { id: conversation.id },
|
{loading && <Spin/>}
|
||||||
},
|
</Space>
|
||||||
{
|
);
|
||||||
subject: TemplateList("messaging").conversation_list.subject,
|
|
||||||
},
|
|
||||||
"e",
|
|
||||||
conversation.id
|
|
||||||
);
|
|
||||||
setLoading(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{loading && <Spin />}
|
|
||||||
</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