- Merge client update into test-beta

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-18 19:20:08 -05:00
696 changed files with 92291 additions and 107075 deletions

View File

@@ -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
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

View File

@@ -2,6 +2,14 @@
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: [
@@ -26,8 +34,10 @@ module.exports = {
lessLoaderOptions: { lessLoaderOptions: {
lessOptions: { lessOptions: {
modifyVars: { modifyVars: {
...v4Token,
// TODO: This will no longer work in AntD 5.0
...(process.env.NODE_ENV === "development" ...(process.env.NODE_ENV === "development"
? { "@primary-color": "#B22234" } ? {"colorPrimary": "#B22234"}
: { : {
//"@primary-color": "#1DA57A" //"@primary-color": "#1DA57A"
}), }),
@@ -53,8 +63,14 @@ module.exports = {
}, },
], ],
webpack: { webpack: {
configure: (webpackConfig) => ({ configure: (webpackConfig) => {
return {
...webpackConfig, ...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: { optimization: {
...webpackConfig.optimization, ...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown // Workaround for CircleCI bug caused by the number of CPUs shown
@@ -67,7 +83,8 @@ module.exports = {
return item; return item;
}), }),
}, },
}), };
},
}, },
devtool: "source-map", devtool: "source-map",
}; };

17
client/cypress.config.js Normal file
View 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',
},
})

View File

@@ -1,8 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -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

15965
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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: {

View File

@@ -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,15 +13,13 @@ 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")
@@ -33,7 +31,6 @@ 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,
@@ -44,14 +41,12 @@ const mapDispatchToProps = (dispatch) => ({
setOnline: (isOnline) => dispatch(setOnline(isOnline)), setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ export function App({bodyshop, checkUserSession, currentUser, online, setOnline}) {
bodyshop,
checkUserSession, const client = useSplitClient().client;
currentUser, const [listenersAdded, setListenersAdded] = useState(false)
online, const {t} = useTranslation();
setOnline,
}) {
const client = useClient();
useEffect(() => { useEffect(() => {
if (!navigator.onLine) { if (!navigator.onLine) {
@@ -64,30 +59,49 @@ export function App({
//const b = Grid.useBreakpoint(); //const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b); // console.log("Breakpoints:", b);
const { t } = useTranslation(); // Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
window.addEventListener("offline", function (e) { const offlineListener = (e) => {
setOnline(false); setOnline(false);
}); }
window.addEventListener("online", function (e) { const onlineListener = (e) => {
setOnline(true); setOnline(true);
}); }
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => { useEffect(() => {
if (currentUser.authorized && bodyshop) { if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid); client.setAttribute("imexshopid", bodyshop.imexshopid);
LogRocket.init("rome-online/rome-online"); if (
if (client.getTreatment("LogRocket_Tracking") === "on") { client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.romeonline.io'
) {
console.log("LR Start");
LogRocket.init("rome-online/rome-online"); LogRocket.init("rome-online/rome-online");
} }
} }
}, [bodyshop, client, currentUser.authorized]); }, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) { if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />; return <LoadingSpinner message={t("general.labels.loggingin")}/>;
} }
handleBeta();
if (!online) if (!online)
return ( return (
<Result <Result
@@ -107,54 +121,27 @@ export function App({
/> />
); );
// Any route that is not assigned and matched will default to the Landing Page component
return ( return (
<Switch> <Suspense fallback={<LoadingSpinner message="Rome Online"/>}>
<Suspense fallback={<LoadingSpinner />}> <Routes>
<ErrorBoundary> <Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
<Route exact path="/" component={LandingPage} /> <Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
</ErrorBoundary> <Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
<ErrorBoundary> <Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route exact path="/signin" component={SignInPage} /> <Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
</ErrorBoundary> <Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<ErrorBoundary> <Route path="/manage/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route exact path="/resetpassword" component={ResetPassword} /> <Route path="*" element={<ManagePage/>}/>
</ErrorBoundary> </Route>
<ErrorBoundary> <Route path="/tech/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route exact path="/csi/:surveyId" component={CsiPage} /> <Route path="*" element={<TechPageContainer/>}/>
</ErrorBoundary> </Route>
<ErrorBoundary> <Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route exact path="/disclaimer" component={DisclaimerPage} /> <Route path="*" element={<DocumentEditorContainer/>}/>
</ErrorBoundary> </Route>
<ErrorBoundary> </Routes>
<Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense> </Suspense>
</Switch>
); );
} }

View File

@@ -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;

View 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;

View File

@@ -1,17 +1,17 @@
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>

View File

@@ -1,18 +1,18 @@
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";
@@ -35,8 +35,8 @@ export function AccountingPayablesTableComponent({
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({
@@ -45,7 +45,7 @@ export function AccountingPayablesTableComponent({
}); });
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 = [
@@ -60,7 +60,7 @@ export function AccountingPayablesTableComponent({
<Link <Link
to={{ to={{
pathname: `/manage/shop/vendors`, pathname: `/manage/shop/vendors`,
search: queryString.stringify({ selectedvendor: record.vendor.id }), search: queryString.stringify({selectedvendor: record.vendor.id}),
}} }}
> >
{record.vendor.name} {record.vendor.name}
@@ -131,7 +131,7 @@ export function AccountingPayablesTableComponent({
state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order, state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Checkbox disabled checked={record.is_credit_memo} /> <Checkbox disabled checked={record.is_credit_memo}/>
), ),
}, },
{ {
@@ -140,7 +140,7 @@ export function AccountingPayablesTableComponent({
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs}/>
), ),
}, },
{ {
@@ -162,7 +162,7 @@ export function AccountingPayablesTableComponent({
]; ];
const handleSearch = (e) => { const handleSearch = (e) => {
setState({ ...state, search: e.target.value }); setState({...state, search: e.target.value});
logImEXEvent("accounting_payables_table_search"); logImEXEvent("accounting_payables_table_search");
}; };
@@ -197,7 +197,7 @@ export function AccountingPayablesTableComponent({
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent/>
)} )}
<Input <Input
value={state.search} value={state.search}
@@ -211,7 +211,7 @@ export function AccountingPayablesTableComponent({
<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}

View File

@@ -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";
@@ -35,8 +35,8 @@ export function AccountingPayablesTableComponent({
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({
@@ -45,7 +45,7 @@ export function AccountingPayablesTableComponent({
}); });
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 = [
@@ -81,11 +81,11 @@ export function AccountingPayablesTableComponent({
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>
); );
}, },
@@ -130,7 +130,7 @@ export function AccountingPayablesTableComponent({
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs}/>
), ),
}, },
{ {
@@ -152,7 +152,7 @@ export function AccountingPayablesTableComponent({
]; ];
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");
}; };
@@ -196,7 +196,7 @@ export function AccountingPayablesTableComponent({
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent/>
)} )}
<Input <Input
value={state.search} value={state.search}
@@ -210,7 +210,7 @@ export function AccountingPayablesTableComponent({
<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}

View File

@@ -1,18 +1,18 @@
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";
@@ -32,8 +32,8 @@ export function AccountingReceivablesTableComponent({
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);
@@ -43,7 +43,7 @@ export function AccountingReceivablesTableComponent({
}); });
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 = [
@@ -89,11 +89,11 @@ export function AccountingReceivablesTableComponent({
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>
); );
}, },
@@ -142,7 +142,7 @@ export function AccountingReceivablesTableComponent({
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs}/>
), ),
}, },
{ {
@@ -167,7 +167,7 @@ export function AccountingReceivablesTableComponent({
]; ];
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");
}; };
@@ -211,7 +211,7 @@ export function AccountingReceivablesTableComponent({
/> />
)} )}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent/>
)} )}
<Input.Search <Input.Search
value={state.search} value={state.search}
@@ -225,7 +225,7 @@ export function AccountingReceivablesTableComponent({
<Table <Table
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top" }} pagination={{position: "top"}}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,4 +1,4 @@
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) {

View File

@@ -1,4 +1,4 @@
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";

View File

@@ -1,9 +1,9 @@
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
@@ -16,11 +16,11 @@ export function AllocationsAssignmentComponent({
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;
@@ -29,7 +29,7 @@ export function AllocationsAssignmentComponent({
<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}
@@ -47,7 +47,7 @@ export function AllocationsAssignmentComponent({
placeholder={t("joblines.fields.mod_lb_hrs")} placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)} max={parseFloat(maxHours)}
min={0} min={0}
onChange={e => setAssignment({ ...assignment, hours: e })} onChange={e => setAssignment({...assignment, hours: e})}
/> />
<Button <Button
@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
); );
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>

View File

@@ -1,7 +1,7 @@
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;

View File

@@ -1,17 +1,17 @@
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),
@@ -20,7 +20,7 @@ export default function AllocationsAssignmentContainer({
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"),
@@ -30,7 +30,7 @@ export default function AllocationsAssignmentContainer({
}) })
.catch((error) => { .catch((error) => {
notification["error"]({ notification["error"]({
message: t("employees.errors.saving", { message: error.message }), message: t("employees.errors.saving", {message: error.message}),
}); });
}); });
}; };

View File

@@ -1,9 +1,9 @@
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,
@@ -19,11 +19,11 @@ export default connect(
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;
@@ -32,7 +32,7 @@ export default connect(
<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}
@@ -59,7 +59,7 @@ export default connect(
); );
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>

View File

@@ -1,16 +1,16 @@
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,
}); });
@@ -26,7 +26,7 @@ export default function AllocationsBulkAssignmentContainer({
return acc; return acc;
}, []); }, []);
insertAllocation({ variables: { alloc: allocs } }).then((r) => { insertAllocation({variables: {alloc: allocs}}).then((r) => {
notification["success"]({ notification["success"]({
message: t("employees.successes.save"), message: t("employees.successes.save"),
}); });

View File

@@ -1,17 +1,17 @@
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}
/> />

View File

@@ -1,17 +1,17 @@
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"),
@@ -19,7 +19,7 @@ export default function AllocationsLabelContainer({ allocation, refetch }) {
if (refetch) refetch(); if (refetch) refetch();
}) })
.catch((error) => { .catch((error) => {
notification["error"]({ message: t("allocations.errors.deleting") }); notification["error"]({message: t("allocations.errors.deleting")});
}); });
}; };

View File

@@ -1,17 +1,17 @@
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"),
@@ -59,23 +59,23 @@ export default function AuditTrailListComponent({ loading, data }) {
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}

View File

@@ -1,24 +1,24 @@
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>

View File

@@ -1,16 +1,16 @@
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"),
@@ -38,23 +38,23 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
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}

View File

@@ -1,13 +1,14 @@
import React from "react"; import React from "react";
import { List } from "antd"; import {List} from "antd";
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { FaArrowRight } from "react-icons/fa"; import {FaArrowRight} from "react-icons/fa";
export default function AuditTrailValuesComponent({ oldV, newV }) {
export default function AuditTrailValuesComponent({oldV, newV}) {
if (!oldV && !newV) return <div></div>; if (!oldV && !newV) return <div></div>;
if (!oldV && newV) if (!oldV && newV)
return ( return (
<List style={{ width: "800px" }} bordered size='small'> <List style={{width: "800px"}} bordered size='small'>
{Object.keys(newV).map((key, idx) => ( {Object.keys(newV).map((key, idx) => (
<List.Item key={idx} value={key}> <List.Item key={idx} value={key}>
{key}: {JSON.stringify(newV[key])} {key}: {JSON.stringify(newV[key])}
@@ -17,10 +18,10 @@ export default function AuditTrailValuesComponent({ oldV, newV }) {
); );
return ( return (
<List style={{ width: "800px" }} bordered size='small'> <List style={{width: "800px"}} bordered size='small'>
{Object.keys(oldV).map((key, idx) => ( {Object.keys(oldV).map((key, idx) => (
<List.Item key={idx}> <List.Item key={idx}>
{key}: {oldV[key]} <Icon component={FaArrowRight} /> {key}: {oldV[key]} <Icon component={FaArrowRight}/>
{JSON.stringify(newV[key])} {JSON.stringify(newV[key])}
</List.Item> </List.Item>
))} ))}

View File

@@ -1,9 +1,10 @@
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}) {
const {t} = useTranslation();
return ( return (
<div> <div>
<Popover <Popover

View File

@@ -1,14 +1,15 @@
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) {
@@ -34,11 +35,11 @@ export default function BillCmdReturnsTableComponent({
return null; return null;
} }
if (returnLoading) return <Skeleton />; if (returnLoading) return <Skeleton/>;
return ( return (
<Form.List name="outstanding_returns"> <Form.List name="outstanding_returns">
{(fields, { add, remove, move }) => { {(fields, {add, remove, move}) => {
return ( return (
<> <>
<Typography.Title level={4}> <Typography.Title level={4}>
@@ -64,7 +65,7 @@ export default function BillCmdReturnsTableComponent({
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>
@@ -75,7 +76,7 @@ export default function BillCmdReturnsTableComponent({
key={`${index}part_type`} key={`${index}part_type`}
name={[field.name, "part_type"]} name={[field.name, "part_type"]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -85,7 +86,7 @@ export default function BillCmdReturnsTableComponent({
key={`${index}quantity`} key={`${index}quantity`}
name={[field.name, "quantity"]} name={[field.name, "quantity"]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -95,7 +96,7 @@ export default function BillCmdReturnsTableComponent({
key={`${index}act_price`} key={`${index}act_price`}
name={[field.name, "act_price"]} name={[field.name, "act_price"]}
> >
<ReadOnlyFormItemComponent type="currency" /> <ReadOnlyFormItemComponent type="currency"/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -105,7 +106,7 @@ export default function BillCmdReturnsTableComponent({
key={`${index}cost`} key={`${index}cost`}
name={[field.name, "cost"]} name={[field.name, "cost"]}
> >
<ReadOnlyFormItemComponent type="currency" /> <ReadOnlyFormItemComponent type="currency"/>
</Form.Item> </Form.Item>
</td> </td>
@@ -117,7 +118,7 @@ export default function BillCmdReturnsTableComponent({
name={[field.name, "cm_received"]} name={[field.name, "cm_received"]}
valuePropName="checked" valuePropName="checked"
> >
<Checkbox /> <Checkbox/>
</Form.Item> </Form.Item>
</td> </td>
</tr> </tr>

View File

@@ -1,30 +1,30 @@
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 }) { search_bills(existingBills, {readField}) {
return existingBills.filter( return existingBills.filter(
(billref) => bill.id !== readField("id", billref) (billref) => bill.id !== readField("id", billref)
); );
@@ -35,7 +35,7 @@ export default function BillDeleteButton({ bill, callback }) {
}); });
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 {
@@ -72,7 +72,7 @@ export default function BillDeleteButton({ bill, callback }) {
// onClick={handleDelete} // onClick={handleDelete}
loading={loading} loading={loading}
> >
<DeleteFilled /> <DeleteFilled/>
</Button> </Button>
</Popconfirm> </Popconfirm>
</RbacWrapper> </RbacWrapper>

View File

@@ -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,15 +22,16 @@ 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(
@@ -42,29 +39,27 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(BillDetailEditcontainer); )(BillDetailEditcontainer);
export function BillDetailEditcontainer({ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { t } = useTranslation(); const {t} = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [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",
}); });
// ... rest of the code remains the same
const handleSave = () => { const handleSave = () => {
//It's got a previously deducted bill line! //It's got a previously deducted bill line!
if ( if (
@@ -72,7 +67,7 @@ export function BillDetailEditcontainer({
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0 0
) )
setVisible(true); setOpen(true);
else { else {
form.submit(); form.submit();
} }
@@ -82,11 +77,11 @@ export function BillDetailEditcontainer({
setUpdateLoading(true); setUpdateLoading(true);
//let adjustmentsToInsert = {}; //let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values; const {billlines, upload, ...bill} = values;
const updates = []; const updates = [];
updates.push( updates.push(
update_bill({ update_bill({
variables: { billId: search.billid, bill: bill }, variables: {billId: search.billid, bill: bill},
}) })
); );
@@ -105,11 +100,11 @@ export function BillDetailEditcontainer({
}); });
deletedJobLines.forEach((d) => { deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } })); updates.push(deleteBillLine({variables: {id: d.id}}));
}); });
billlines.forEach((billline) => { billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline; const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename; delete il.__typename;
if (il.id) { if (il.id) {
@@ -155,18 +150,18 @@ export function BillDetailEditcontainer({
await refetch(); await refetch();
form.setFieldsValue(transformData(data)); form.setFieldsValue(transformData(data));
form.resetFields(); form.resetFields();
setVisible(false); setOpen(false);
setUpdateLoading(false); setUpdateLoading(false);
}; };
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>; if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported; const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return ( return (
<> <>
{loading && <LoadingSkeleton />} {loading && <LoadingSkeleton/>}
{data && ( {data && (
<> <>
<PageHeader <PageHeader
@@ -176,13 +171,13 @@ export function BillDetailEditcontainer({
} }
extra={ extra={
<Space> <Space>
<BillDetailEditReturn data={data} /> <BillDetailEditReturn data={data}/>
<BillPrintButton billid={search.billid} /> <BillPrintButton billid={search.billid}/>
<Popconfirm <Popconfirm
visible={visible} open={open}
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
onCancel={() => setVisible(false)} onCancel={() => setOpen(false)}
okButtonProps={{ loading: updateLoading }} okButtonProps={{loading: updateLoading}}
title={t("bills.labels.editadjwarning")} title={t("bills.labels.editadjwarning")}
> >
<Button <Button
@@ -195,8 +190,8 @@ export function BillDetailEditcontainer({
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
</Popconfirm> </Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} /> <BillReeportButtonComponent bill={data && data.bills_by_pk}/>
<BillMarkExportedButton bill={data && data.bills_by_pk} /> <BillMarkExportedButton bill={data && data.bills_by_pk}/>
</Space> </Space>
} }
/> />
@@ -206,11 +201,11 @@ export function BillDetailEditcontainer({
initialValues={transformData(data)} initialValues={transformData(data)}
layout="vertical" layout="vertical"
> >
<BillFormContainer form={form} billEdit disabled={exported} /> <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}
/> />
@@ -246,7 +241,7 @@ const transformData = (data) => {
}, },
}; };
}), }),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null, date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
} }
: {}; : {};
}; };

View File

@@ -1,13 +1,13 @@
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({
@@ -15,9 +15,9 @@ const mapStateToProps = createStructuredSelector({
}); });
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(
@@ -31,14 +31,14 @@ export function BillDetailEditReturn({
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({
@@ -67,18 +67,18 @@ export function BillDetailEditReturn({
}); });
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()}
@@ -89,9 +89,9 @@ export function BillDetailEditReturn({
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>
@@ -124,7 +124,7 @@ export function BillDetailEditReturn({
name={[field.name, "selected"]} name={[field.name, "selected"]}
valuePropName="checked" valuePropName="checked"
> >
<Checkbox /> <Checkbox/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -133,7 +133,7 @@ export function BillDetailEditReturn({
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>
@@ -142,7 +142,7 @@ export function BillDetailEditReturn({
key={`${index}quantity`} key={`${index}quantity`}
name={[field.name, "quantity"]} name={[field.name, "quantity"]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -151,7 +151,7 @@ export function BillDetailEditReturn({
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>
@@ -160,7 +160,7 @@ export function BillDetailEditReturn({
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>
@@ -175,7 +175,7 @@ export function BillDetailEditReturn({
<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")}

View File

@@ -1,12 +1,12 @@
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])
@@ -29,12 +29,12 @@ export default function BillDetailEditcontainer() {
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>
); );
} }

View File

@@ -1,35 +1,29 @@
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,
@@ -38,8 +32,8 @@ const mapStateToProps = createStructuredSelector({
}); });
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");
@@ -50,9 +44,9 @@ function BillEnterModalContainer({
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);
@@ -64,11 +58,13 @@ function BillEnterModalContainer({
"enter_bill_generate_label", "enter_bill_generate_label",
false false
); );
const { Enhanced_Payroll } = useTreatments(
["Enhanced_Payroll"], const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
{}, attributes: {},
bodyshop.imexshopid names: ["Enhanced_Payroll"],
); splitKey: bodyshop.imexshopid,
});
const formValues = useMemo(() => { const formValues = useMemo(() => {
return { return {
...billEnterModal.context.bill, ...billEnterModal.context.bill,
@@ -222,7 +218,7 @@ function BillEnterModalContainer({
mutation: UPDATE_JOB, mutation: UPDATE_JOB,
variables: { variables: {
jobId: values.jobid, jobId: values.jobid,
job: { lbr_adjustments: newAdjustments }, job: {lbr_adjustments: newAdjustments},
}, },
}); });
if (!!jobUpdate.errors) { if (!!jobUpdate.errors) {
@@ -241,7 +237,7 @@ function BillEnterModalContainer({
if (markPolReceived && markPolReceived.length > 0) { if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({ const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) }, variables: {partsLineIds: markPolReceived.map((p) => p.id)},
}); });
if (!!r2.errors) { if (!!r2.errors) {
setLoading(false); setLoading(false);
@@ -321,7 +317,7 @@ function BillEnterModalContainer({
if (bodyshop.uselocalmediaserver) { if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => { upload.forEach((u) => {
handleLocalUpload({ handleLocalUpload({
ev: { file: u.originFileObj }, ev: {file: u.originFileObj},
context: { context: {
jobid: values.jobid, jobid: values.jobid,
invoice_number: remainingValues.invoice_number, invoice_number: remainingValues.invoice_number,
@@ -332,7 +328,7 @@ function BillEnterModalContainer({
} else { } else {
upload.forEach((u) => { upload.forEach((u) => {
handleUpload( handleUpload(
{ file: u.originFileObj }, {file: u.originFileObj},
{ {
bodyshop: bodyshop, bodyshop: bodyshop,
uploaded_by: currentUser.email, uploaded_by: currentUser.email,
@@ -399,18 +395,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]); }, [enterAgain, form]);
useEffect(() => { useEffect(() => {
if (billEnterModal.visible) { if (billEnterModal.open) {
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
} else { } else {
form.resetFields(); form.resetFields();
} }
}, [billEnterModal.visible, form, formValues]); }, [billEnterModal.open, form, formValues]);
return ( return (
<Modal <Modal
title={t("bills.labels.new")} title={t("bills.labels.new")}
width={"98%"} width={"98%"}
visible={billEnterModal.visible} open={billEnterModal.open}
okText={t("general.actions.save")} okText={t("general.actions.save")}
keyboard="false" keyboard="false"
onOk={() => form.submit()} onOk={() => form.submit()}

View File

@@ -1,18 +1,19 @@
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"), title: t("joblines.fields.line_desc"),
@@ -79,7 +80,7 @@ export default function BillFormLinesExtended({
{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>
) : ( ) : (
<></> <></>
@@ -122,7 +123,7 @@ export default function BillFormLinesExtended({
return ( return (
<Form.Item noStyle name="billlineskeys"> <Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button> <button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear /> <Input onChange={(e) => setSearch(e.target.value)} allowClear/>
<Table <Table
pagination={false} pagination={false}
size="small" size="small"

View File

@@ -1,15 +1,11 @@
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";
@@ -33,10 +29,10 @@ export function BillFormItemsExtendedFormItem({
disabled, disabled,
responsibilityCenters, responsibilityCenters,
discount, discount,
}) { }) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys"); // const { billlineskeys } = form.getFieldsValue("billlineskeys");
const { t } = useTranslation(); const {t} = useTranslation();
if (!value) if (!value)
return ( return (
<Button <Button
@@ -64,7 +60,7 @@ export function BillFormItemsExtendedFormItem({
}); });
}} }}
> >
<PlusCircleFilled /> <PlusCircleFilled/>
</Button> </Button>
); );
@@ -74,13 +70,13 @@ export function BillFormItemsExtendedFormItem({
label={t("billlines.fields.line_desc")} label={t("billlines.fields.line_desc")}
name={["billlineskeys", record.id, "line_desc"]} name={["billlineskeys", record.id, "line_desc"]}
> >
<Input disabled={disabled} /> <Input disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.quantity")} label={t("billlines.fields.quantity")}
name={["billlineskeys", record.id, "quantity"]} name={["billlineskeys", record.id, "quantity"]}
> >
<InputNumber precision={0} min={0} disabled={disabled} /> <InputNumber precision={0} min={0} disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.actual_price")} label={t("billlines.fields.actual_price")}
@@ -90,7 +86,7 @@ export function BillFormItemsExtendedFormItem({
min={0} min={0}
disabled={disabled} disabled={disabled}
onBlur={(e) => { onBlur={(e) => {
const { billlineskeys } = form.getFieldsValue("billlineskeys"); const {billlineskeys} = form.getFieldsValue("billlineskeys");
form.setFieldsValue({ form.setFieldsValue({
billlineskeys: { billlineskeys: {
...billlineskeys, ...billlineskeys,
@@ -113,7 +109,7 @@ export function BillFormItemsExtendedFormItem({
label={t("billlines.fields.actual_cost")} label={t("billlines.fields.actual_cost")}
name={["billlineskeys", record.id, "actual_cost"]} name={["billlineskeys", record.id, "actual_cost"]}
> >
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
@@ -124,15 +120,15 @@ export function BillFormItemsExtendedFormItem({
Math.round((line.actual_cost / line.actual_price) * 100) / 100 Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2); ).toPrecision(2);
if (lineDiscount - discount === 0) return <div />; if (lineDiscount - discount === 0) return <div/>;
return <WarningOutlined style={{ color: "red" }} />; return <WarningOutlined style={{color: "red"}}/>;
}} }}
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.cost_center")} label={t("billlines.fields.cost_center")}
name={["billlineskeys", record.id, "cost_center"]} name={["billlineskeys", record.id, "cost_center"]}
> >
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{minWidth: "3rem"}} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false) ? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ( : responsibilityCenters.costs.map((item) => (
@@ -157,9 +153,9 @@ export function BillFormItemsExtendedFormItem({
name={["billlineskeys", record.id, "deductedfromlbr"]} name={["billlineskeys", record.id, "deductedfromlbr"]}
valuePropName="checked" valuePropName="checked"
> >
<Switch disabled={disabled} /> <Switch disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item shouldUpdate style={{ display: "inline-block" }}> <Form.Item shouldUpdate style={{display: "inline-block"}}>
{() => { {() => {
if ( if (
form.getFieldsValue("billlineskeys").billlineskeys[record.id] form.getFieldsValue("billlineskeys").billlineskeys[record.id]
@@ -238,7 +234,7 @@ export function BillFormItemsExtendedFormItem({
}, },
]} ]}
> >
<InputNumber precision={2} min={0.01} /> <InputNumber precision={2} min={0.01}/>
</Form.Item> </Form.Item>
</div> </div>
); );
@@ -251,21 +247,21 @@ export function BillFormItemsExtendedFormItem({
name={["billlineskeys", record.id, "applicable_taxes", "federal"]} name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
valuePropName="checked" valuePropName="checked"
> >
<Switch disabled={disabled} /> <Switch disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.state_tax_applicable")} label={t("billlines.fields.state_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "state"]} name={["billlineskeys", record.id, "applicable_taxes", "state"]}
valuePropName="checked" valuePropName="checked"
> >
<Switch disabled={disabled} /> <Switch disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("billlines.fields.local_tax_applicable")} label={t("billlines.fields.local_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "local"]} name={["billlineskeys", record.id, "applicable_taxes", "local"]}
valuePropName="checked" valuePropName="checked"
> >
<Switch disabled={disabled} /> <Switch disabled={disabled}/>
</Form.Item> </Form.Item>
<Button <Button
@@ -281,7 +277,7 @@ export function BillFormItemsExtendedFormItem({
}); });
}} }}
> >
<MinusCircleFilled /> <MinusCircleFilled/>
</Button> </Button>
</Space> </Space>
); );

View File

@@ -1,26 +1,16 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, {UploadOutlined} from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import {useApolloClient} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
Alert, import dayjs from "../../utils/day";
Divider, import React, {useEffect, useState} from "react";
Form, import {useTranslation} from "react-i18next";
Input, import {MdOpenInNew} from "react-icons/md";
Select, import {connect} from "react-redux";
Space, import {Link} from "react-router-dom";
Statistic, import {createStructuredSelector} from "reselect";
Switch, import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
Upload, import {selectBodyshop} from "../../redux/user/user.selectors";
} from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -30,7 +20,7 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import {CalculateBillTotal} from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -50,21 +40,19 @@ export function BillFormComponent({
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake, preferredMake
}) { }) {
const { t } = useTranslation();
const {t} = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const [discount, setDiscount] = useState(0); const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"], const {treatments: {Extended_Bill_Posting, ClosingPeriod}} = useSplitTreatments({
{}, attributes: {},
bodyshop.imexshopid names: ["Extended_Bill_Posting", "ClosingPeriod"],
); splitKey: bodyshop.imexshopid,
const { ClosingPeriod } = useTreatments( });
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
@@ -109,7 +97,7 @@ export function BillFormComponent({
} }
const jobId = form.getFieldValue("jobid"); const jobId = form.getFieldValue("jobid");
if (jobId) { if (jobId) {
loadLines({ variables: { id: jobId } }); loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) { if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({ loadOutstandingReturns({
variables: { variables: {
@@ -136,13 +124,13 @@ export function BillFormComponent({
return ( return (
<div> <div>
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form}/>
<Form.Item <Form.Item
style={{ display: "none" }} style={{display: "none"}}
name="isinhouse" name="isinhouse"
valuePropName="checked" valuePropName="checked"
> >
<Switch /> <Switch/>
</Form.Item> </Form.Item>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -161,7 +149,7 @@ export function BillFormComponent({
notExported={false} notExported={false}
onBlur={() => { onBlur={() => {
if (form.getFieldValue("jobid") !== null) { if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } }); loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) { if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({ loadOutstandingReturns({
variables: { variables: {
@@ -183,7 +171,7 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({getFieldValue}) => ({
validator(rule, value) { validator(rule, value) {
if ( if (
value && value &&
@@ -222,7 +210,7 @@ export function BillFormComponent({
> >
<Space> <Space>
{iou.ro_number} {iou.ro_number}
<Icon component={MdOpenInNew} /> <Icon component={MdOpenInNew}/>
</Space> </Space>
</Link> </Link>
</Space> </Space>
@@ -240,7 +228,7 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({getFieldValue}) => ({
async validator(rule, value) { async validator(rule, value) {
const vendorid = getFieldValue("vendorid"); const vendorid = getFieldValue("vendorid");
if (vendorid && value) { if (vendorid && value) {
@@ -271,7 +259,7 @@ export function BillFormComponent({
}), }),
]} ]}
> >
<Input disabled={disabled || disableInvNumber} /> <Input disabled={disabled || disableInvNumber}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.date")} label={t("bills.fields.date")}
@@ -281,24 +269,24 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({getFieldValue}) => ({
validator(rule, value) { validator(rule, value) {
if ( if (
ClosingPeriod.treatment === "on" && ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod bodyshop.accountingconfig.ClosingPeriod
) { ) {
if ( if (
moment(value) dayjs(value)
.startOf("day") .startOf("day")
.isSameOrAfter( .isSameOrAfter(
moment( dayjs(
bodyshop.accountingconfig.ClosingPeriod[0] bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day") ).startOf("day")
) && ) &&
moment(value) dayjs(value)
.startOf("day") .startOf("day")
.isSameOrBefore( .isSameOrBefore(
moment( dayjs(
bodyshop.accountingconfig.ClosingPeriod[1] bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day") ).endOf("day")
) )
@@ -314,14 +302,14 @@ export function BillFormComponent({
}), }),
]} ]}
> >
<FormDatePicker disabled={disabled} /> <FormDatePicker disabled={disabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.is_credit_memo")} label={t("bills.fields.is_credit_memo")}
name="is_credit_memo" name="is_credit_memo"
valuePropName="checked" valuePropName="checked"
rules={[ rules={[
({ getFieldValue }) => ({ ({getFieldValue}) => ({
validator(rule, value) { validator(rule, value) {
if ( if (
value === true && value === true &&
@@ -353,7 +341,7 @@ export function BillFormComponent({
}), }),
]} ]}
> >
<Switch /> <Switch/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.total")} label={t("bills.fields.total")}
@@ -365,11 +353,11 @@ export function BillFormComponent({
}, },
]} ]}
> >
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled}/>
</Form.Item> </Form.Item>
{!billEdit && ( {!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location"> <Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear> <Select style={{width: "10rem"}} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}> <Select.Option key={idx} value={loc}>
{loc} {loc}
@@ -394,29 +382,24 @@ export function BillFormComponent({
label={t("bills.fields.state_tax_rate")} label={t("bills.fields.state_tax_rate")}
name="state_tax_rate" name="state_tax_rate"
> >
<CurrencyInput min={0} disabled={disabled} /> <CurrencyInput min={0} disabled={disabled}/>
</Form.Item> </Form.Item>
{ {/*<Form.Item*/}
// <Form.Item {/* span={3}*/}
// span={3} {/* label={t("bills.fields.local_tax_rate")}*/}
// label={t("bills.fields.local_tax_rate")} {/* name="local_tax_rate"*/}
// name="local_tax_rate" {/*>*/}
// > {/* <CurrencyInput min={0} />*/}
// <CurrencyInput min={0} /> {/*</Form.Item>*/}
// </Form.Item> {/* {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (*/}
} {/* <Form.Item*/}
{ {/* span={2}*/}
//Removed as a part of the merge to Rome Online. Federal tax not applicable. {/* label={t("bills.labels.federal_tax_exempt")}*/}
// bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( {/* name="federal_tax_exempt"*/}
// <Form.Item {/* >*/}
// span={2} {/* <Switch onChange={handleFederalTaxExemptSwitchToggle} />*/}
// label={t("bills.labels.federal_tax_exempt")} {/* </Form.Item>*/}
// name="federal_tax_exempt" {/* ) : null}*/}
// >
// <Switch onChange={handleFederalTaxExemptSwitchToggle} />
// </Form.Item>
// ) : null
}
<Form.Item shouldUpdate span={13}> <Form.Item shouldUpdate span={13}>
{() => { {() => {
const values = form.getFieldsValue([ const values = form.getFieldsValue([
@@ -519,7 +502,7 @@ export function BillFormComponent({
<Form.Item <Form.Item
name="upload" name="upload"
label="Upload" label="Upload"
style={{ display: billEdit ? "none" : null }} style={{display: billEdit ? "none" : null}}
valuePropName="fileList" valuePropName="fileList"
getValueFromEvent={(e) => { getValueFromEvent={(e) => {
if (Array.isArray(e)) { if (Array.isArray(e)) {
@@ -536,7 +519,7 @@ export function BillFormComponent({
> >
<> <>
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<UploadOutlined /> <UploadOutlined/>
</p> </p>
<p className="ant-upload-text"> <p className="ant-upload-text">
Click or drag files to this area to upload. Click or drag files to this area to upload.

View File

@@ -1,13 +1,13 @@
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";
@@ -22,25 +22,25 @@ export function BillFormContainer({
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 (
@@ -79,4 +79,5 @@ export function BillFormContainer({
</> </>
); );
} }
export default connect(mapStateToProps, null)(BillFormContainer); export default connect(mapStateToProps, null)(BillFormContainer);

View File

@@ -1,22 +1,11 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import {DeleteFilled, DollarCircleFilled} from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react"; import {useSplitTreatments} from "@splitsoftware/splitio-react";
import { import {Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip,} from "antd";
Button,
Checkbox,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip,
} from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import {selectBodyshop} from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect"; import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
@@ -39,20 +28,16 @@ export function BillEnterModalLinesComponent({
responsibilityCenters, responsibilityCenters,
billEdit, billEdit,
billid, billid,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const {setFieldsValue, getFieldsValue, getFieldValue} = form;
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"], const {treatments: {Simple_Inventory, Enhanced_Payroll}} = useSplitTreatments({
{}, attributes: {},
bodyshop && bodyshop.imexshopid names: ["Simple_Inventory", "Enhanced_Payroll"],
); splitKey: bodyshop && bodyshop.imexshopid,
});
const { Enhanced_Payroll } = useTreatments(
["Enhanced_Payroll"],
{},
bodyshop.imexshopid
);
const columns = (remove) => { const columns = (remove) => {
return [ return [
@@ -90,7 +75,7 @@ export function BillEnterModalLinesComponent({
<BillLineSearchSelect <BillLineSearchSelect
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ width: "100%", minWidth: "10rem" }} style={{width: "100%", minWidth: "10rem"}}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => { onSelect={(value, opt) => {
setFieldsValue({ setFieldsValue({
@@ -142,7 +127,7 @@ export function BillEnterModalLinesComponent({
], ],
}; };
}, },
formInput: (record, index) => <Input disabled={disabled} />, formInput: (record, index) => <Input disabled={disabled}/>,
}, },
{ {
title: t("billlines.fields.quantity"), title: t("billlines.fields.quantity"),
@@ -159,7 +144,7 @@ export function BillEnterModalLinesComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({ ({getFieldValue}) => ({
validator(rule, value) { validator(rule, value) {
if ( if (
value && value &&
@@ -181,7 +166,7 @@ export function BillEnterModalLinesComponent({
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
<InputNumber precision={0} min={1} disabled={disabled} /> <InputNumber precision={0} min={1} disabled={disabled}/>
), ),
}, },
{ {
@@ -255,7 +240,7 @@ export function BillEnterModalLinesComponent({
valuePropName="checked" valuePropName="checked"
name={[record.name, "create_ppc"]} name={[record.name, "create_ppc"]}
> >
<Checkbox /> <Checkbox/>
</Form.Item> </Form.Item>
{t("joblines.fields.create_ppc")} {t("joblines.fields.create_ppc")}
</Space> </Space>
@@ -360,7 +345,7 @@ export function BillEnterModalLinesComponent({
}; };
}, },
formInput: (record, index) => ( formInput: (record, index) => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{minWidth: "3rem"}} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false) ? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ( : responsibilityCenters.costs.map((item) => (
@@ -405,9 +390,9 @@ export function BillEnterModalLinesComponent({
name: [field.name, "deductedfromlbr"], name: [field.name, "deductedfromlbr"],
}; };
}, },
formInput: (record, index) => <Switch disabled={disabled} />, formInput: (record, index) => <Switch disabled={disabled}/>,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}> <Form.Item shouldUpdate noStyle style={{display: "inline-block"}}>
{() => { {() => {
const price = getFieldValue([ const price = getFieldValue([
"billlines", "billlines",
@@ -529,7 +514,7 @@ export function BillEnterModalLinesComponent({
}, },
]} ]}
> >
<InputNumber precision={2} min={0.01} /> <InputNumber precision={2} min={0.01}/>
</Form.Item> </Form.Item>
)} )}
@@ -572,7 +557,7 @@ export function BillEnterModalLinesComponent({
name: [field.name, "applicable_taxes", "state"], name: [field.name, "applicable_taxes", "state"],
}; };
}, },
formInput: (record, index) => <Switch disabled={disabled} />, formInput: (record, index) => <Switch disabled={disabled}/>,
}, },
// { // {
// title: t("billlines.fields.local_tax_applicable"), // title: t("billlines.fields.local_tax_applicable"),
@@ -604,7 +589,7 @@ export function BillEnterModalLinesComponent({
} }
onClick={() => remove(record.name)} onClick={() => remove(record.name)}
> >
<DeleteFilled /> <DeleteFilled/>
</Button> </Button>
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<BilllineAddInventory <BilllineAddInventory
@@ -656,7 +641,7 @@ export function BillEnterModalLinesComponent({
}, },
]} ]}
> >
{(fields, { add, remove, move }) => { {(fields, {add, remove, move}) => {
return ( return (
<> <>
<Table <Table
@@ -669,7 +654,7 @@ export function BillEnterModalLinesComponent({
bordered bordered
dataSource={fields} dataSource={fields}
columns={mergedColumns(remove)} columns={mergedColumns(remove)}
scroll={{ x: true }} scroll={{x: true}}
pagination={false} pagination={false}
rowClassName="editable-row" rowClassName="editable-row"
/> />
@@ -679,7 +664,7 @@ export function BillEnterModalLinesComponent({
onClick={() => { onClick={() => {
add(); add();
}} }}
style={{ width: "100%" }} style={{width: "100%"}}
> >
{t("billlines.actions.newline")} {t("billlines.actions.newline")}
</Button> </Button>
@@ -708,14 +693,14 @@ const EditableCell = ({
additional, additional,
wrapper, wrapper,
...restProps ...restProps
}) => { }) => {
if (additional) if (additional)
return ( return (
<td {...restProps}> <td {...restProps}>
<div size="small"> <div size="small">
<Form.Item <Form.Item
name={dataIndex} name={dataIndex}
labelCol={{ span: 0 }} labelCol={{span: 0}}
{...(formItemProps && formItemProps(record))} {...(formItemProps && formItemProps(record))}
> >
{(formInput && formInput(record, record.name)) || children} {(formInput && formInput(record, record.name)) || children}
@@ -729,7 +714,7 @@ const EditableCell = ({
<wrapper> <wrapper>
<td {...restProps}> <td {...restProps}>
<Form.Item <Form.Item
labelCol={{ span: 0 }} labelCol={{span: 0}}
name={dataIndex} name={dataIndex}
{...(formItemProps && formItemProps(record))} {...(formItemProps && formItemProps(record))}
> >
@@ -741,7 +726,7 @@ const EditableCell = ({
return ( return (
<td {...restProps}> <td {...restProps}>
<Form.Item <Form.Item
labelCol={{ span: 0 }} labelCol={{span: 0}}
name={dataIndex} name={dataIndex}
{...(formItemProps && formItemProps(record))} {...(formItemProps && formItemProps(record))}
> >

View File

@@ -1,14 +1,14 @@
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;
@@ -31,7 +31,7 @@ export const CalculateBillTotal = (invoice) => {
} }
}); });
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);

View File

@@ -1,13 +1,13 @@
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,
@@ -25,8 +25,8 @@ export function BillInventoryTable({
billEdit, billEdit,
inventoryLoading, inventoryLoading,
inventoryData, inventoryData,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
useEffect(() => { useEffect(() => {
if (inventoryData && inventoryData.inventory) { if (inventoryData && inventoryData.inventory) {
@@ -55,11 +55,11 @@ export function BillInventoryTable({
return null; return null;
} }
if (inventoryLoading) return <Skeleton />; if (inventoryLoading) return <Skeleton/>;
return ( return (
<Form.List name="inventory"> <Form.List name="inventory">
{(fields, { add, remove, move }) => { {(fields, {add, remove, move}) => {
return ( return (
<> <>
<Typography.Title level={4}> <Typography.Title level={4}>
@@ -86,7 +86,7 @@ export function BillInventoryTable({
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>
@@ -103,7 +103,7 @@ export function BillInventoryTable({
"name", "name",
]} ]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -113,7 +113,7 @@ export function BillInventoryTable({
key={`${index}quantity`} key={`${index}quantity`}
name={[field.name, "quantity"]} name={[field.name, "quantity"]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -123,7 +123,7 @@ export function BillInventoryTable({
key={`${index}act_price`} key={`${index}act_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>
@@ -133,7 +133,7 @@ export function BillInventoryTable({
key={`${index}cost`} key={`${index}cost`}
name={[field.name, "actual_cost"]} name={[field.name, "actual_cost"]}
> >
<ReadOnlyFormItemComponent type="currency" /> <ReadOnlyFormItemComponent type="currency"/>
</Form.Item> </Form.Item>
</td> </td>
<td> <td>
@@ -143,7 +143,7 @@ export function BillInventoryTable({
key={`${index}comment`} key={`${index}comment`}
name={[field.name, "comment"]} name={[field.name, "comment"]}
> >
<ReadOnlyFormItemComponent /> <ReadOnlyFormItemComponent/>
</Form.Item> </Form.Item>
</td> </td>
@@ -155,7 +155,7 @@ export function BillInventoryTable({
name={[field.name, "consumefrominventory"]} name={[field.name, "consumefrominventory"]}
valuePropName="checked" valuePropName="checked"
> >
<Checkbox /> <Checkbox/>
</Form.Item> </Form.Item>
</td> </td>
</tr> </tr>

View File

@@ -1,21 +1,22 @@
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}
optionLabelProp={"name"}
// optionFilterProp="line_desc" // optionFilterProp="line_desc"
filterOption={(inputValue, option) => { filterOption={(inputValue, option) => {
return ( return (
@@ -55,8 +56,11 @@ const BillLineSearchSelect = (
alt_partno={item.alt_partno} alt_partno={item.alt_partno}
act_price={item.act_price} act_price={item.act_price}
style={{ style={{
...(item.removed ? { textDecoration: "line-through" } : {}), ...(item.removed ? {textDecoration: "line-through"} : {}),
}} }}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
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}${
@@ -64,12 +68,12 @@ const BillLineSearchSelect = (
}${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)}`
: ``} : ``}

View File

@@ -1,18 +1,14 @@
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,
@@ -32,8 +28,8 @@ export function BillMarkExportedButton({
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);
@@ -52,7 +48,7 @@ export function BillMarkExportedButton({
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: { billId: bill.id }, variables: {billId: bill.id},
}); });
await insertExportLog({ await insertExportLog({

View File

@@ -1,11 +1,11 @@
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");

View File

@@ -1,16 +1,13 @@
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,
@@ -24,8 +21,8 @@ export default connect(
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`
@@ -43,7 +40,7 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
const handleUpdate = async () => { const handleUpdate = async () => {
setLoading(true); setLoading(true);
const result = await updateBill({ const result = await updateBill({
variables: { billId: bill.id }, variables: {billId: bill.id},
}); });
if (!result.errors) { if (!result.errors) {

View File

@@ -1,19 +1,16 @@
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,
@@ -33,10 +30,9 @@ export function BilllineAddInventory({
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 () => { const addToInventory = async () => {
@@ -50,7 +46,7 @@ export function BilllineAddInventory({
jobid: jobid, jobid: jobid,
isinhouse: true, isinhouse: true,
is_credit_memo: true, is_credit_memo: true,
date: moment().format("YYYY-MM-DD"), date: dayjs().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
@@ -88,11 +84,11 @@ export function BilllineAddInventory({
quantity: billline.quantity, quantity: billline.quantity,
line_desc: billline.line_desc, line_desc: billline.line_desc,
}, },
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert. cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
pol: { pol: {
returnfrombill: billid, returnfrombill: billid,
vendorid: bodyshop.inhousevendorid, vendorid: bodyshop.inhousevendorid,
deliver_by: moment().format("YYYY-MM-DD"), deliver_by: dayjs().format("YYYY-MM-DD"),
parts_order_lines: { parts_order_lines: {
data: [ data: [
{ {
@@ -145,7 +141,7 @@ export function BilllineAddInventory({
} }
onClick={addToInventory} onClick={addToInventory}
> >
<FileAddFilled /> <FileAddFilled/>
{billline?.inventories?.length > 0 && ( {billline?.inventories?.length > 0 && (
<div>({billline?.inventories?.length} in inv)</div> <div>({billline?.inventories?.length} in inv)</div>
)} )}

View File

@@ -1,16 +1,16 @@
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";
@@ -22,11 +22,11 @@ const mapStateToProps = createStructuredSelector({
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({
@@ -38,8 +38,8 @@ export function BillsListTableComponent({
setPartsOrderContext, setPartsOrderContext,
setBillEnterContext, setBillEnterContext,
setReconciliationContext, setReconciliationContext,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
@@ -50,17 +50,17 @@ export function BillsListTableComponent({
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> <Space wrap>
{showView && ( {showView && (
<Button onClick={() => handleOnRowClick(record)}> <Button onClick={() => handleOnRowClick(record)}>
<EditFilled /> <EditFilled/>
</Button> </Button>
)} )}
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record}/>
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }} data={{bills_by_pk: {...record, jobid: job.id}}}
disabled={ disabled={
record.is_credit_memo || record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid || record.vendorid === bodyshop.inhousevendorid ||
@@ -72,9 +72,9 @@ export function BillsListTableComponent({
<PrintWrapperComponent <PrintWrapperComponent
templateObject={{ templateObject={{
name: Templates.inhouse_invoice.key, name: Templates.inhouse_invoice.key,
variables: { id: record.id }, variables: {id: record.id},
}} }}
messageObject={{ subject: Templates.inhouse_invoice.subject }} messageObject={{subject: Templates.inhouse_invoice.subject}}
/> />
)} )}
</Space> </Space>
@@ -126,7 +126,7 @@ export function BillsListTableComponent({
sortOrder: sortOrder:
state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.columnKey === "is_credit_memo" &&
state.sortedInfo.order, state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo} />, render: (text, record) => <Checkbox checked={record.is_credit_memo}/>,
}, },
{ {
title: t("bills.fields.exported"), title: t("bills.fields.exported"),
@@ -135,7 +135,7 @@ export function BillsListTableComponent({
sorter: (a, b) => a.exported - b.exported, sorter: (a, b) => a.exported - b.exported,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order, state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported} />, render: (text, record) => <Checkbox checked={record.exported}/>,
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -146,7 +146,7 @@ export function BillsListTableComponent({
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({...state, filteredInfo: filters, sortedInfo: sorter});
}; };
const filteredBills = bills const filteredBills = bills
@@ -173,14 +173,14 @@ export function BillsListTableComponent({
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined/>
</Button> </Button>
{job && job.converted ? ( {job && job.converted ? (
<> <>
<Button <Button
onClick={() => { onClick={() => {
setBillEnterContext({ setBillEnterContext({
actions: { refetch: billsQuery.refetch }, actions: {refetch: billsQuery.refetch},
context: { context: {
job, job,
}, },
@@ -192,7 +192,7 @@ export function BillsListTableComponent({
<Button <Button
onClick={() => { onClick={() => {
setReconciliationContext({ setReconciliationContext({
actions: { refetch: billsQuery.refetch }, actions: {refetch: billsQuery.refetch},
context: { context: {
job, job,
bills: (billsQuery.data && billsQuery.data.bills) || [], bills: (billsQuery.data && billsQuery.data.bills) || [],
@@ -229,6 +229,7 @@ export function BillsListTableComponent({
</Card> </Card>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps

View File

@@ -1,23 +1,23 @@
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: {},
@@ -25,7 +25,7 @@ export default function BillsVendorsList() {
}); });
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 = [
@@ -57,19 +57,19 @@ export default function BillsVendorsList() {
delete search.billid; delete search.billid;
if (record.id) { if (record.id) {
search.vendorid = record.id; search.vendorid = record.id;
history.push({ search: queryString.stringify(search) }); history.push({search: queryString.stringify(search)});
} }
} else { } else {
delete search.vendorid; delete search.vendorid;
history.push({ search: queryString.stringify(search) }); history.push({search: queryString.stringify(search)});
} }
}; };
const handleSearch = (e) => { const handleSearch = (e) => {
setState({ ...state, search: e.target.value }); setState({...state, search: e.target.value});
}; };
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error"/>;
const dataSource = state.search const dataSource = state.search
? data.vendors.filter( ? data.vendors.filter(
@@ -98,7 +98,7 @@ export default function BillsVendorsList() {
); );
}} }}
dataSource={dataSource} dataSource={dataSource}
pagination={{ position: "top" }} pagination={{position: "top"}}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -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
);
const {treatments: {OpenSearch}} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid,
});
// TODO - Client Update - Technically key is not doing anything here
return ( return (
<Row className="breadcrumb-container"> <Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}> <Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">"> <Breadcrumb
<Breadcrumb.Item> separator=">"
<Link to={`/manage`}> items={[
<HomeFilled />{" "} {
key: "home",
title: (
<Link to={`/manage/`}>
<HomeFilled/>{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""} ""}
</Link> </Link>
</Breadcrumb.Item> ),
{breadcrumbs.map((item) => },
item.link ? ( ...breadcrumbs.map((item) =>
<Breadcrumb.Item key={item.label}> item.link
<Link to={item.link}>{item.label} </Link> ? {
</Breadcrumb.Item> key: item.label,
) : ( title: <Link to={item.link}>{item.label}</Link>,
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item> }
) : {
)} key: item.label,
</Breadcrumb> title: item.label,
}
),
]}
/>
</Col> </Col>
<Col xs={24} sm={24} md={8}> <Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />} {OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
</Col> </Col>
</Row> </Row>
); );
} }
export default connect(mapStateToProps, null)(BreadCrumbs); export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -1,13 +1,13 @@
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({
@@ -22,10 +22,10 @@ const mapDispatchToProps = (dispatch) => ({
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;
@@ -35,7 +35,7 @@ export function ContractsFindModalContainer({
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();
@@ -63,14 +63,14 @@ export function ContractsFindModalContainer({
}; };
useEffect(() => { useEffect(() => {
if (visible) { if (open) {
form.resetFields(); form.resetFields();
} }
}, [visible, form]); }, [open, form]);
return ( return (
<Modal <Modal
visible={visible} open={open}
width="70%" width="70%"
title={t("payments.labels.findermodal")} title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()} onCancel={() => toggleModalVisible()}
@@ -84,7 +84,7 @@ export function ContractsFindModalContainer({
autoComplete="no" autoComplete="no"
onFinish={handleFinish} onFinish={handleFinish}
> >
<CaBcEtfTableModalComponent form={form} /> <CaBcEtfTableModalComponent form={form}/>
<Button onClick={() => form.submit()} type="primary" loading={loading}> <Button onClick={() => form.submit()} type="primary" loading={loading}>
{t("general.labels.search")} {t("general.labels.search")}
</Button> </Button>

View File

@@ -1,9 +1,9 @@
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,
@@ -11,8 +11,8 @@ const mapStateToProps = createStructuredSelector({
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>
@@ -25,7 +25,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
}, },
]} ]}
> >
<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")}

View File

@@ -1,30 +1,31 @@
import React, { useState } from "react"; import React, {useState} from "react";
import { Button, Form, InputNumber, Popover } from "antd"; import {Button, Form, InputNumber, Popover} from "antd";
import { logImEXEvent } from "../../firebase/firebase.utils"; import {logImEXEvent} from "../../firebase/firebase.utils";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { CalculatorFilled } from "@ant-design/icons"; import {CalculatorFilled} from "@ant-design/icons";
export default function CABCpvrtCalculator({ disabled, form }) {
export default function CABCpvrtCalculator({disabled, form}) {
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
const { t } = useTranslation(); const {t} = useTranslation();
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate"); logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ form.setFieldsValue({
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2), ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
}); });
form.setFields([{ name: "ca_bc_pvrt", touched: true }]); form.setFields([{name: "ca_bc_pvrt", touched: true}]);
setVisibility(false); setVisibility(false);
}; };
const popContent = ( const popContent = (
<div> <div>
<Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}> <Form onFinish={handleFinish} initialValues={{rate: 1.5}}>
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}> <Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
<InputNumber precision={2} min={0} /> <InputNumber precision={2} min={0}/>
</Form.Item> </Form.Item>
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}> <Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
<InputNumber precision={0} min={0} /> <InputNumber precision={0} min={0}/>
</Form.Item> </Form.Item>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
{t("general.actions.calculate")} {t("general.actions.calculate")}
@@ -38,11 +39,11 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Popover <Popover
destroyTooltipOnHide destroyTooltipOnHide
content={popContent} content={popContent}
visible={visibility} open={visibility}
disabled={disabled} disabled={disabled}
> >
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled /> <CalculatorFilled/>
</Button> </Button>
</Popover> </Popover>
); );

View File

@@ -1,32 +1,18 @@
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";
@@ -37,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
}); });
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")),
}); });
@@ -47,20 +33,20 @@ const CardPaymentModalComponent = ({
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,
} }
); );
@@ -82,7 +68,7 @@ const CardPaymentModalComponent = ({
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: {
@@ -117,7 +103,7 @@ const CardPaymentModalComponent = ({
payer: t("payments.labels.customer"), payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand, type: values.paymentResponse.cardbrand,
jobid: payment.jobid, jobid: payment.jobid,
date: moment(Date.now()), date: dayjs(Date.now()),
payment_responses: { payment_responses: {
data: [ data: [
{ {
@@ -141,7 +127,7 @@ const CardPaymentModalComponent = ({
console.error(error); console.error(error);
notification.open({ notification.open({
type: "error", type: "error",
message: t("payments.errors.inserting", { error: error.message }), message: t("payments.errors.inserting", {error: error.message}),
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@@ -195,11 +181,11 @@ const CardPaymentModalComponent = ({
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [], payments: context.jobid ? [{jobid: context.jobid}] : [],
}} }}
> >
<Form.List name={["payments"]}> <Form.List name={["payments"]}>
{(fields, { add, remove, move }) => { {(fields, {add, remove, move}) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -235,12 +221,12 @@ const CardPaymentModalComponent = ({
}, },
]} ]}
> >
<CurrencyFormItemComponent /> <CurrencyFormItemComponent/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={2}> <Col span={2}>
<DeleteFilled <DeleteFilled
style={{ margin: "1rem" }} style={{margin: "1rem"}}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
@@ -255,7 +241,7 @@ const CardPaymentModalComponent = ({
onClick={() => { onClick={() => {
add(); add();
}} }}
style={{ width: "100%" }} style={{width: "100%"}}
> >
{t("general.actions.add")} {t("general.actions.add")}
</Button> </Button>
@@ -274,13 +260,13 @@ const CardPaymentModalComponent = ({
{() => { {() => {
console.log("Updating the owner info section."); console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field. //If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue(); const {payments} = form.getFieldsValue();
if ( if (
payments?.length > 0 && payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length payments?.filter((p) => p?.jobid).length === payments?.length
) { ) {
console.log("**Calling refetch."); console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) }); refetch({jobids: payments.map((p) => p.jobid)});
} }
console.log( console.log(
"Acc info", "Acc info",
@@ -324,13 +310,13 @@ const CardPaymentModalComponent = ({
} }
> >
{() => { {() => {
const { payments } = form.getFieldsValue(); const {payments} = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => { const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0); return acc + (val?.amount || 0);
}, 0); }, 0);
return ( return (
<Space style={{ float: "right" }}> <Space style={{float: "right"}}>
<Statistic <Statistic
title="Amount To Charge" title="Amount To Charge"
value={totalAmountToCharge} value={totalAmountToCharge}
@@ -360,7 +346,7 @@ const CardPaymentModalComponent = ({
{/* Lightbox payment response when it is completed */} {/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden> <Form.Item name="paymentResponse" hidden>
<Input type="hidden" /> <Input type="hidden"/>
</Form.Item> </Form.Item>
</Form> </Form>
</Spin> </Spin>

View File

@@ -1,11 +1,11 @@
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({
@@ -21,9 +21,9 @@ 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();
@@ -35,7 +35,7 @@ function CardPaymentModalContainer({
return ( return (
<Modal <Modal
open={visible} open={open}
onOk={handleOK} onOk={handleOK}
onCancel={handleCancel} onCancel={handleCancel}
footer={[ footer={[
@@ -46,7 +46,7 @@ function CardPaymentModalContainer({
width="80%" width="80%"
destroyOnClose destroyOnClose
> >
<CardPaymentModalComponent /> <CardPaymentModalComponent/>
</Modal> </Modal>
); );
} }

View File

@@ -1,25 +1,16 @@
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,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
useEffect(() => { useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return; if (!bodyshop || !bodyshop.messagingservicesid) return;
@@ -47,7 +38,6 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
<Button <Button
onClick={async () => { onClick={async () => {
await requestForToken(); await requestForToken();
SubscribeToTopic(); SubscribeToTopic();
}} }}
> >
@@ -81,16 +71,17 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data, payload: (payload && payload.data && payload.data.data) || payload.data,
}); });
} }
let stopMessageListenr, channel;
let stopMessageListener, channel;
try { try {
stopMessageListenr = onMessage(messaging, handleMessage); stopMessageListener = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages"); channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage); channel.addEventListener("message", handleMessage);
} catch (error) { } catch (error) {
console.log("Unable to set event listeners."); console.log("Unable to set event listeners.");
} }
return () => { return () => {
stopMessageListenr && stopMessageListenr(); stopMessageListener && stopMessageListener();
channel && channel.removeEventListener("message", handleMessage); channel && channel.removeEventListener("message", handleMessage);
}; };
}, [client]); }, [client]);
@@ -99,8 +90,9 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null} {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
</div> </div>
); );
} }
export default connect(mapStateToProps, null)(ChatAffixContainer);
export default ChatAffixContainer;

View File

@@ -1,18 +1,18 @@
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"],
}); });

View File

@@ -1,19 +1,14 @@
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({
@@ -30,14 +25,44 @@ function ChatConversationListComponent({
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 ( return (
<CellMeasurer <CellMeasurer
@@ -49,44 +74,21 @@ function ChatConversationListComponent({
> >
<List.Item <List.Item
onClick={() => setSelectedConversation(item.id)} onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${ style={style}
className={`chat-list-item
${
item.id === selectedConversation item.id === selectedConversation
? "chat-list-selected-conversation" ? "chat-list-selected-conversation"
: null : null
}`} }`}
style={style}
> >
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
{cardContentLeft}
</div>
<div <div
style={{ style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
display: "inline-block", </Card>
}}
>
{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> </List.Item>
</CellMeasurer> </CellMeasurer>
); );
@@ -95,14 +97,14 @@ function ChatConversationListComponent({
return ( return (
<div className="chat-list-container"> <div className="chat-list-container">
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({height, width}) => (
<VirtualizedList <VirtualizedList
height={height} height={height}
width={width} width={width}
rowCount={conversationList.length} rowCount={conversationList.length}
rowHeight={cache.rowHeight} rowHeight={cache.rowHeight}
rowRenderer={rowRenderer} rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => { onScroll={({scrollTop, scrollHeight, clientHeight}) => {
if (scrollTop + clientHeight === scrollHeight) { if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations(); loadMoreConversations();
} }

View File

@@ -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;
} }

View File

@@ -1,12 +1,12 @@
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) => {
@@ -19,7 +19,7 @@ export default function ChatConversationTitleTags({ jobConversations }) {
}, },
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);
@@ -42,12 +42,12 @@ export default function ChatConversationTitleTags({ jobConversations }) {
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>
))} ))}

View File

@@ -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>
); );
} }

View File

@@ -11,11 +11,11 @@ export default function ChatConversationComponent({
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
@@ -23,9 +23,9 @@ export default function ChatConversationComponent({
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>
); );
} }

View File

@@ -1,16 +1,14 @@
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,
@@ -18,21 +16,21 @@ const mapStateToProps = createStructuredSelector({
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},
} }
); );
@@ -41,7 +39,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
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({
@@ -51,7 +49,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
}), }),
fields: { fields: {
messages_aggregate(cached) { messages_aggregate(cached) {
return { aggregate: { count: 0 } }; return {aggregate: {count: 0}};
}, },
}, },
}); });

View File

@@ -1,22 +1,23 @@
import { PlusOutlined } from "@ant-design/icons"; import {PlusOutlined} from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import {useMutation} from "@apollo/client";
import { Input, notification, Spin, Tag, Tooltip } from "antd"; import {Input, notification, Spin, Tag, Tooltip} from "antd";
import React, { useState } from "react"; import React, {useState} from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; import {UPDATE_CONVERSATION_LABEL} from "../../graphql/conversations.queries";
export default function ChatLabel({ conversation }) {
export default function ChatLabel({conversation}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [value, setValue] = useState(conversation.label); const [value, setValue] = useState(conversation.label);
const { t } = useTranslation(); const {t} = useTranslation();
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL); const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
const handleSave = async () => { const handleSave = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await updateLabel({ const response = await updateLabel({
variables: { id: conversation.id, label: value }, variables: {id: conversation.id, label: value},
}); });
if (response.errors) { if (response.errors) {
notification["error"]({ notification["error"]({
@@ -47,18 +48,18 @@ export default function ChatLabel({ conversation }) {
onBlur={handleSave} onBlur={handleSave}
allowClear allowClear
/> />
{loading && <Spin size="small" />} {loading && <Spin size="small"/>}
</div> </div>
); );
} else { } else {
return conversation.label && conversation.label.trim() !== "" ? ( return conversation.label && conversation.label.trim() !== "" ? (
<Tag style={{ cursor: "pointer" }} onClick={() => setEditing(true)}> <Tag style={{cursor: "pointer"}} onClick={() => setEditing(true)}>
{conversation.label} {conversation.label}
</Tag> </Tag>
) : ( ) : (
<Tooltip title={t("messaging.labels.addlabel")}> <Tooltip title={t("messaging.labels.addlabel")}>
<PlusOutlined <PlusOutlined
style={{ cursor: "pointer" }} style={{cursor: "pointer"}}
onClick={() => setEditing(true)} onClick={() => setEditing(true)}
/> />
</Tooltip> </Tooltip>

View File

@@ -1,15 +1,16 @@
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({
@@ -25,11 +26,11 @@ export function ChatMediaSelector({
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: {
@@ -39,13 +40,13 @@ export function ChatMediaSelector({
}, },
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(() => {
@@ -54,10 +55,10 @@ export function ChatMediaSelector({
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
@@ -65,7 +66,7 @@ export function ChatMediaSelector({
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
/> />
)} )}
{bodyshop.uselocalmediaserver && visible && ( {bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={ jobId={
@@ -88,11 +89,11 @@ export function ChatMediaSelector({
} }
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>
); );

View File

@@ -1,19 +1,14 @@
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({
@@ -32,10 +27,10 @@ export default function ChatMessageListComponent({ messages }) {
useEffect(scrollToBottom, [messages]); useEffect(scrollToBottom, [messages]);
const _rowRenderer = ({ index, key, parent, style }) => { const _rowRenderer = ({index, key, parent, style}) => {
return ( return (
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}> <CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
{({ measure, registerChild }) => ( {({measure, registerChild}) => (
<div <div
ref={registerChild} ref={registerChild}
onLoad={measure} onLoad={measure}
@@ -49,10 +44,10 @@ export default function ChatMessageListComponent({ messages }) {
{StatusRender(messages[index].status)} {StatusRender(messages[index].status)}
</div> </div>
{messages[index].isoutbound && ( {messages[index].isoutbound && (
<div style={{ fontSize: 10 }}> <div style={{fontSize: 10}}>
{i18n.t("messaging.labels.sentby", { {i18n.t("messaging.labels.sentby", {
by: messages[index].userid, by: messages[index].userid,
time: moment(messages[index].created_at).format( time: dayjs(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a" "MM/DD/YYYY @ hh:mm a"
), ),
})} })}
@@ -67,7 +62,7 @@ export default function ChatMessageListComponent({ messages }) {
return ( return (
<div className="chat"> <div className="chat">
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({height, width}) => (
<List <List
ref={virtualizedListRef} ref={virtualizedListRef}
width={width} width={width}
@@ -87,16 +82,16 @@ export default function ChatMessageListComponent({ messages }) {
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>
))} ))}
@@ -109,9 +104,9 @@ const MessageRender = (message) => {
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;
} }

View File

@@ -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;

View File

@@ -1,13 +1,11 @@
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
@@ -16,11 +14,11 @@ 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();
}; };
@@ -31,11 +29,11 @@ export function ChatNewConversation({ openChatByPhone }) {
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")}
@@ -46,7 +44,7 @@ export function ChatNewConversation({ openChatByPhone }) {
return ( return (
<Popover trigger="click" content={popContent}> <Popover trigger="click" content={popContent}>
<PlusCircleFilled /> <PlusCircleFilled/>
</Popover> </Popover>
); );
} }

View File

@@ -1,14 +1,15 @@
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,
@@ -24,8 +25,8 @@ export function ChatOpenButton({
phone, phone,
jobid, jobid,
openChatByPhone, openChatByPhone,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
if (!phone) return <></>; if (!phone) return <></>;
if (!bodyshop.messagingservicesid) if (!bodyshop.messagingservicesid)
@@ -39,9 +40,9 @@ export function ChatOpenButton({
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")});
} }
}} }}
> >
@@ -49,4 +50,5 @@ export function ChatOpenButton({
</a> </a>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton); export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);

View File

@@ -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";
@@ -37,22 +26,22 @@ 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 }] = const [getConversations, {loading, data, refetch, fetchMore}] =
useLazyQuery(CONVERSATION_LIST_QUERY, { useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
skip: !chatVisible, skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}), ...(pollInterval > 0 ? {pollInterval} : {}),
}); });
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
@@ -94,12 +83,12 @@ export function ChatPopupComponent({
<Typography.Title level={4}> <Typography.Title level={4}>
{t("messaging.labels.messaging")} {t("messaging.labels.messaging")}
</Typography.Title> </Typography.Title>
<ChatNewConversation /> <ChatNewConversation/>
<Tooltip title={t("messaging.labels.recentonly")}> <Tooltip title={t("messaging.labels.recentonly")}>
<InfoCircleOutlined /> <InfoCircleOutlined/>
</Tooltip> </Tooltip>
<SyncOutlined <SyncOutlined
style={{ cursor: "pointer" }} style={{cursor: "pointer"}}
onClick={() => refetch()} onClick={() => refetch()}
/> />
{pollInterval > 0 && ( {pollInterval > 0 && (
@@ -108,13 +97,13 @@ export function ChatPopupComponent({
</Space> </Space>
<ShrinkOutlined <ShrinkOutlined
onClick={() => toggleChatVisible()} onClick={() => toggleChatVisible()}
style={{ position: "absolute", right: ".5rem", top: ".5rem" }} style={{position: "absolute", right: ".5rem", top: ".5rem"}}
/> />
<Row gutter={[8, 8]} className="chat-popup-content"> <Row gutter={[8, 8]} className="chat-popup-content">
<Col span={8}> <Col span={8}>
{loading ? ( {loading ? (
<LoadingSpinner /> <LoadingSpinner/>
) : ( ) : (
<ChatConversationListComponent <ChatConversationListComponent
conversationList={data ? data.conversations : []} conversationList={data ? data.conversations : []}
@@ -123,16 +112,16 @@ export function ChatPopupComponent({
)} )}
</Col> </Col>
<Col span={16}> <Col span={16}>
{selectedConversation ? <ChatConversationContainer /> : null} {selectedConversation ? <ChatConversationContainer/> : null}
</Col> </Col>
</Row> </Row>
</div> </div>
) : ( ) : (
<div <div
onClick={() => toggleChatVisible()} onClick={() => toggleChatVisible()}
style={{ cursor: "pointer" }} style={{cursor: "pointer"}}
> >
<MessageOutlined className="chat-popup-info-icon" /> <MessageOutlined className="chat-popup-info-icon"/>
<strong>{t("messaging.labels.messaging")}</strong> <strong>{t("messaging.labels.messaging")}</strong>
</div> </div>
)} )}
@@ -140,4 +129,5 @@ export function ChatPopupComponent({
</Badge> </Badge>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent); export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);

View File

@@ -13,6 +13,7 @@
height: 100%; height: 100%;
} }
} }
.chat-popup-info-icon { .chat-popup-info-icon {
margin-right: 5px; margin-right: 5px;
} }

View File

@@ -1,10 +1,10 @@
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
@@ -15,25 +15,23 @@ const mapDispatchToProps = (dispatch) => ({
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> const items = bodyshop.md_messaging_presets.map((i, idx) => ({
{bodyshop.md_messaging_presets.map((i, idx) => ( key: idx,
<Menu.Item onClick={() => setMessage(i.text)} key={idx}> label: (i.label),
{i.label} onClick: () => setMessage(i.text),
</Menu.Item> }));
))}
</Menu>
);
return ( return (
<div className={className}> <div className={className}>
<Dropdown trigger={["click"]} overlay={menu}> <Dropdown trigger={["click"]} menu={{items}}>
<PlusCircleOutlined /> <PlusCircleOutlined/>
</Dropdown> </Dropdown>
</div> </div>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps

View File

@@ -1,11 +1,11 @@
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({});
@@ -13,47 +13,34 @@ const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e)),
}); });
export function ChatPrintButton({ conversation }) { export function ChatPrintButton({conversation}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const generateDocument = (type) => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: {id: conversation.id},
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
type,
conversation.id
).catch(e => {
console.warn('Something went wrong generating a document.');
});
setLoading(false);
}
return ( return (
<Space wrap> <Space wrap>
<PrinterOutlined <PrinterOutlined onClick={() => generateDocument('p')}/>
onClick={() => { <MailOutlined onClick={() => generateDocument('e')}/>
setLoading(true); {loading && <Spin/>}
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"p",
conversation.id
);
setLoading(false);
}}
/>
<MailOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"e",
conversation.id
);
setLoading(false);
}}
/>
{loading && <Spin />}
</Space> </Space>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton); export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);

View File

@@ -1,19 +1,13 @@
import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; import {LoadingOutlined, SendOutlined} from "@ant-design/icons";
import { Input, Spin } from "antd"; import {Input, Spin} from "antd";
import React, { useEffect, useRef, useState } from "react"; import React, {useEffect, useRef, 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 { import {sendMessage, setMessage,} from "../../redux/messaging/messaging.actions";
sendMessage, import {selectIsSending, selectMessage,} from "../../redux/messaging/messaging.selectors";
setMessage, import {selectBodyshop} from "../../redux/user/user.selectors";
} from "../../redux/messaging/messaging.actions";
import {
selectIsSending,
selectMessage,
} from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component";
@@ -35,14 +29,14 @@ function ChatSendMessageComponent({
isSending, isSending,
message, message,
setMessage, setMessage,
}) { }) {
const inputArea = useRef(null); const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]); const [selectedMedia, setSelectedMedia] = useState([]);
useEffect(() => { useEffect(() => {
inputArea.current.focus(); inputArea.current.focus();
}, [isSending, setMessage]); }, [isSending, setMessage]);
const { t } = useTranslation(); const {t} = useTranslation();
const handleEnter = () => { const handleEnter = () => {
const selectedImages = selectedMedia.filter((i) => i.isSelected); const selectedImages = selectedMedia.filter((i) => i.isSelected);
@@ -60,27 +54,27 @@ function ChatSendMessageComponent({
}); });
setSelectedMedia( setSelectedMedia(
selectedMedia.map((i) => { selectedMedia.map((i) => {
return { ...i, isSelected: false }; return {...i, isSelected: false};
}) })
); );
} }
}; };
return ( return (
<div className="imex-flex-row" style={{ width: "100%" }}> <div className="imex-flex-row" style={{width: "100%"}}>
<ChatPresetsComponent className="imex-flex-row__margin" /> <ChatPresetsComponent className="imex-flex-row__margin"/>
<ChatMediaSelector <ChatMediaSelector
conversation={conversation} conversation={conversation}
selectedMedia={selectedMedia} selectedMedia={selectedMedia}
setSelectedMedia={setSelectedMedia} setSelectedMedia={setSelectedMedia}
/> />
<span style={{ flex: 1 }}> <span style={{flex: 1}}>
<Input.TextArea <Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow" className="imex-flex-row__margin imex-flex-row__grow"
allowClear allowClear
autoFocus autoFocus
ref={inputArea} ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }} autoSize={{minRows: 1, maxRows: 4}}
value={message} value={message}
disabled={isSending} disabled={isSending}
placeholder={t("messaging.labels.typeamessage")} placeholder={t("messaging.labels.typeamessage")}
@@ -97,7 +91,7 @@ function ChatSendMessageComponent({
onClick={handleEnter} onClick={handleEnter}
/> />
<Spin <Spin
style={{ display: `${isSending ? "" : "none"}` }} style={{display: `${isSending ? "" : "none"}`}}
indicator={ indicator={
<LoadingOutlined <LoadingOutlined
style={{ style={{
@@ -110,6 +104,7 @@ function ChatSendMessageComponent({
</div> </div>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps

View File

@@ -1,30 +1,30 @@
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons"; import {CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons";
import { Empty, Select, Space } from "antd"; import {Empty, Select, Space} from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
export default function ChatTagRoComponent({ export default function ChatTagRoComponent({
roOptions, roOptions,
loading, loading,
handleSearch, handleSearch,
handleInsertTag, handleInsertTag,
setVisible, setOpen,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
return ( return (
<Space flex> <Space>
<div style={{ width: "15rem" }}> <div style={{width: "15rem"}}>
<Select <Select
showSearch showSearch
autoFocus autoFocus
dropdownMatchSelectWidth popupMatchSelectWidth
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
filterOption={false} filterOption={false}
onSearch={handleSearch} onSearch={handleSearch}
onSelect={handleInsertTag} onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />} notFoundContent={loading ? <LoadingOutlined/> : <Empty/>}
> >
{roOptions.map((item, idx) => ( {roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}> <Select.Option key={item.id || idx}>
@@ -33,12 +33,12 @@ export default function ChatTagRoComponent({
))} ))}
</Select> </Select>
</div> </div>
{loading ? <LoadingOutlined /> : null} {loading ? <LoadingOutlined/> : null}
{loading ? ( {loading ? (
<LoadingOutlined /> <LoadingOutlined/>
) : ( ) : (
<CloseCircleOutlined onClick={() => setVisible(false)} /> <CloseCircleOutlined onClick={() => setOpen(false)}/>
)} )}
</Space> </Space>
); );

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