Compare commits

..

2 Commits

Author SHA1 Message Date
Patrick Fic
b5a2be9392 Package updates. 2022-04-18 10:27:06 -07:00
Patrick Fic
df1e15be60 All package upgrades. 2022-04-13 17:46:46 -07:00
1050 changed files with 167039 additions and 144862 deletions

View File

@@ -1,207 +0,0 @@
version: 2.1
orbs:
#snyk: snyk/snyk@0.0.8
#cypress: cypress-io/cypress@1.23.0
aws-s3: circleci/aws-s3@4.0.0
eb: circleci/aws-elastic-beanstalk@2.0.1
jobs:
api-deploy:
docker:
- image: cimg/node:18.18.2
steps:
- checkout
- eb/setup
- run:
command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status
hasura-migrate:
docker:
- image: cimg/node:16.15.0
parameters:
secret:
type: string
default: $HASURA_PROD_SECRET
working_directory: ~/repo/hasura
steps:
- checkout:
path: ~/repo
- run:
name: Execute migration
command: |
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
app-build:
docker:
- image: cimg/node:16.15.0
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-production/"
arguments: "--exclude '*.map'"
app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: xlarge
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/"
arguments: "--exclude '*.map'"
test-hasura-migrate:
docker:
- image: cimg/node:16.15.0
parameters:
secret:
type: string
default: $HASURA_TEST_SECRET
working_directory: ~/repo/hasura
steps:
- checkout:
path: ~/repo
- run:
name: Execute migration
command: |
npm install hasura-cli -g
echo ${HASURA_TEST_SECRET}
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
test-app-build:
docker:
- image: cimg/node:16.15.0
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/"
arguments: "--exclude '*.map'"
test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: snaptsoft/pfic
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/"
arguments: "--exclude '*.map'"
admin-app-build:
docker:
- image: cimg/node:16.15.0
working_directory: ~/repo/admin
steps:
- checkout:
path: ~/repo
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: npm i
- save_cache:
paths:
- node_modules
- ~/.npm
- ~/.cache
key: v1-dependencies-{{ checksum "package.json" }}
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://adm.imex.online/"
workflows:
deploy_and_build:
jobs:
- api-deploy:
filters:
branches:
only: master
- app-build:
filters:
branches:
only: master
- app-beta-build:
filters:
branches:
only: master-beta
- hasura-migrate:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master
- test-app-build:
filters:
branches:
only: test
- test-app-beta-build:
filters:
branches:
only: test-beta
- test-hasura-migrate:
secret: ${HASURA_TEST_SECRET}
filters:
branches:
only: test
#- admin-app-build:
#filters:
#branches:
#only: master

View File

@@ -1,20 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console": "off"
},
"settings": {}
}

7
.gitignore vendored
View File

@@ -113,10 +113,3 @@ firebase/.env
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
logs/oAuthClient-log.log
.node-persist/**
/*.env.*
.idea/*
.idea

1
.npmrc
View File

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

View File

@@ -1,3 +1,14 @@
Yarn Dependency Management:
To force upgrades for some packages:
yarn upgrade-interactive --latest
To Start Hasura CLI:
npx hasura console
Migrating to Staging:
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
NGROK TEsting:
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
@@ -8,6 +19,3 @@ npx deadfile ./src/index.js --exclude build templates
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
hasura migrate apply --version "1620771761757" --skip-execution --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
Generate the license file:
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite

68
admin/README.md Normal file
View File

@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `yarn build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

18103
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
admin/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.3.15",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.5",
"@types/prop-types": "^15.7.3",
"apollo-boost": "^0.4.9",
"apollo-link-context": "^1.0.20",
"apollo-link-logger": "^2.0.0",
"dotenv": "^8.2.0",
"firebase": "^8.4.1",
"graphql": "^15.4.0",
"prop-types": "^15.7.2",
"ra-data-hasura-graphql": "^0.1.13",
"react": "^17.0.1",
"react-admin": "^3.14.4",
"react-dom": "^17.0.1",
"react-icons": "^4.2.0",
"react-scripts": "4.0.3",
"sass": "^1.32.10"
},
"scripts": {
"start": "set PORT=3001 && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
admin/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

43
admin/public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>ImEX Online - ADMIN</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
admin/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
admin/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
admin/public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
admin/src/App/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

12
admin/src/App/App.js Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import AdminRoot from "../components/admin-root/admin-root.component";
import "./App.css";
function App() {
return (
<div className="App">
<AdminRoot />
</div>
);
}
export default App;

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,283 @@
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import { ApolloLink } from "apollo-boost";
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http";
import apolloLogger from "apollo-link-logger";
import buildHasuraProvider from "ra-data-hasura-graphql";
import React, { Component } from "react";
import {
Admin,
EditGuesser,
ListGuesser,
Resource,
ShowGuesser,
} from "react-admin";
import { FaFileInvoiceDollar } from "react-icons/fa";
import CircularProgress from "@material-ui/core/CircularProgress";
import { auth } from "../../firebase/admin-firebase-utils";
import authProvider from "../auth-provider/auth-provider";
import JoblinesCreate from "../joblines/joblines.create";
import JoblinesEdit from "../joblines/joblines.edit";
import JoblinesList from "../joblines/joblines.list";
import JoblinesShow from "../joblines/joblines.show";
import JobsCreate from "../jobs/jobs.create";
import JobsEdit from "../jobs/jobs.edit";
import JobsList from "../jobs/jobs.list";
import JobsShow from "../jobs/jobs.show";
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
headers: {
"x-hasura-admin-secret": `Dev-BodyShopApp!`,
// 'Authorization': `Bearer xxxx`,
},
});
const authLink = setContext((_, { headers }) => {
return (
auth.currentUser &&
auth.currentUser.getIdToken().then((token) => {
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
} else {
return { headers };
}
})
);
});
const middlewares = [];
if (process.env.NODE_ENV === "development") {
middlewares.push(apolloLogger);
}
middlewares.push(authLink.concat(httpLink));
const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache: new InMemoryCache(),
});
// const client = new ApolloClient({
// uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
// cache: new InMemoryCache(),
// headers: {
// "x-hasura-admin-secret": `Dev-BodyShopApp!`,
// // 'Authorization': `Bearer xxxx`,
// },
// });
class AdminRoot extends Component {
constructor() {
super();
this.state = { dataProvider: null };
}
componentDidMount() {
buildHasuraProvider({
client,
}).then((dataProvider) => this.setState({ dataProvider }));
}
render() {
const { dataProvider } = this.state;
if (!dataProvider) {
return (
<div>
<CircularProgress />
</div>
);
}
return (
<ApolloProvider client={client}>
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource
icon={FaFileInvoiceDollar}
name="jobs"
list={JobsList}
edit={JobsEdit}
create={JobsCreate}
show={JobsShow}
/>
<Resource
name="joblines"
list={JoblinesList}
edit={JoblinesEdit}
create={JoblinesCreate}
show={JoblinesShow}
/>
<Resource
name="bodyshops"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="owners"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="vehicles"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="appointments"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="available_jobs"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="cccontracts"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="conversations"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="counters"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="courtesycars"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="csi"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="csiquestions"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="documents"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="employees"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="invoicelines"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="invoices"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="job_conversations"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="masterdata"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="messages"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="notes"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="parts_order_lines"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="parts_orders"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="payments"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="scoreboard"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="templates"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="timetickets"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="users"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
<Resource
name="vendors"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
</Admin>
</ApolloProvider>
);
}
}
export default AdminRoot;

View File

@@ -0,0 +1,39 @@
import { auth, getCurrentUser } from "../../firebase/admin-firebase-utils";
const authProvider = {
login: async ({ username, password }) => {
console.log(username, password);
try {
const { user } = await auth.signInWithEmailAndPassword(
username,
password
);
const token = await user.getIdToken();
localStorage.setItem("token", token);
return Promise.resolve();
} catch (error) {
console.log("error", error);
return Promise.reject();
}
},
logout: async (params) => {
await auth.signOut();
localStorage.removeItem("token");
return Promise.resolve();
},
checkAuth: async (params) => {
const user = await getCurrentUser();
if (!!user) {
return Promise.resolve();
} else {
return Promise.reject();
}
},
checkError: (error) => {
return Promise.resolve();
},
getPermissions: (params) => {
return Promise.resolve();
},
};
export default authProvider;

View File

@@ -0,0 +1,26 @@
import React from "react";
import {
Create,
NumberInput, SimpleForm,
TextInput
} from "react-admin";
const JoblinesCreate = (props) => (
<Create {...props}>
<SimpleForm>
<TextInput source="line_ref" />
<TextInput source="line_ind" />
<NumberInput source="db_price" />
<NumberInput source="act_price" />
<NumberInput source="part_qty" />
<NumberInput source="mod_lb_hrs" />
<TextInput source="mod_lbr_type" />
<TextInput source="lbr_op" />
</SimpleForm>
</Create>
);
export default JoblinesCreate;

View File

@@ -0,0 +1,70 @@
import React from "react";
import {
BooleanInput,
DateField,
Edit,
NumberInput,
SimpleForm,
TextInput,
} from "react-admin";
const JoblinesEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<TextInput source="id" />
<DateField showTime source="created_at" />
<DateField showTime source="updated_at" />
<TextInput source="jobid" />
<NumberInput source="unq_seq" />
<NumberInput source="line_ind" />
<TextInput source="line_desc" />
<TextInput source="part_type" />
<TextInput source="oem_partno" />
<TextInput source="est_seq" />
<TextInput source="db_ref" />
<TextInput source="line_ref" />
<BooleanInput source="tax_part" />
<NumberInput source="db_price" />
<NumberInput source="act_price" />
<NumberInput source="part_qty" />
<TextInput source="alt_partno" />
<TextInput source="mod_lbr_ty" />
<NumberInput source="db_hrs" />
<NumberInput source="mod_lb_hrs" />
<TextInput source="lbr_op" />
<NumberInput source="lbr_amt" />
<BooleanInput source="glass_flag" />
<TextInput source="price_inc" />
<TextInput source="alt_part_i" />
<TextInput source="price_j" />
<TextInput source="cert_part" />
<TextInput source="alt_co_id" />
<TextInput source="alt_overrd" />
<TextInput source="alt_partm" />
<TextInput source="prt_dsmk_p" />
<TextInput source="prt_dsmk_m" />
<TextInput source="lbr_inc" />
<TextInput source="lbr_hrs_j" />
<TextInput source="lbr_typ_j" />
<TextInput source="lbr_op_j" />
<TextInput source="paint_stg" />
<TextInput source="paint_tone" />
<TextInput source="lbr_tax" />
<NumberInput source="misc_amt" />
<TextInput source="misc_sublt" />
<TextInput source="misc_tax" />
<TextInput source="bett_type" />
<NumberInput source="bett_pctg" />
<NumberInput source="bett_amt" />
<TextInput source="bett_tax" />
<TextInput source="op_code_desc" />
<TextInput source="status" />
<TextInput source="removed" />
<NumberInput source="line_no" />
<TextInput source="notes" />
<TextInput source='"location"' />
</SimpleForm>
</Edit>
);
export default JoblinesEdit;

View File

@@ -0,0 +1,29 @@
import React from "react";
import {
Datagrid, List,
NumberField,
ReferenceField, TextField
} from "react-admin";
const JoblinesList = (props) => (
<List {...props}>
<Datagrid rowClick="edit">
<ReferenceField source="jobid" reference="jobs">
<TextField source="ro_number" />
</ReferenceField>
<TextField source="line_ref" />
<TextField source="line_ind" />
<NumberField source="db_price" />
<NumberField source="act_price" />
<NumberField source="part_qty" />
<NumberField source="mod_lb_hrs" />
<TextField source="mod_lbr_type" />
<TextField source="lbr_op" />
</Datagrid>
</List>
);
export default JoblinesList;

View File

@@ -0,0 +1,24 @@
import React from "react";
import {
NumberInput, Show,
SimpleShowLayout,
TextInput
} from "react-admin";
const JoblinesShow = (props) => (
<Show {...props}>
<SimpleShowLayout>
<TextInput source="line_ref" />
<TextInput source="line_ind" />
<NumberInput source="db_price" />
<NumberInput source="act_price" />
<NumberInput source="part_qty" />
<NumberInput source="mod_lb_hrs" />
<TextInput source="mod_lbr_type" />
<TextInput source="lbr_op" />
</SimpleShowLayout>
</Show>
);
export default JoblinesShow;

View File

@@ -0,0 +1,17 @@
import React from "react";
import { Create, EmailField, SimpleForm, TextInput } from "react-admin";
const JobsCreate = (props) => (
<Create {...props}>
<SimpleForm>
<TextInput source="ro_number" />
<TextInput source="ownr_fn" />
<TextInput source="ownr_ln" />
<TextInput source="converted" />
<EmailField source="ownr_ea" />
</SimpleForm>
</Create>
);
export default JobsCreate;

View File

@@ -0,0 +1,316 @@
import React from "react";
//@ts-ignore
import {
AutocompleteInput,
BooleanInput,
Edit,
FormTab,
NumberInput,
ReferenceInput,
SelectInput,
SimpleForm,
TabbedForm,
TextInput,
} from "react-admin";
const JobsEdit = (props) => (
<Edit {...props}>
<TabbedForm>
<FormTab label="Job Info">
<SimpleForm>
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
<SelectInput disabled optionText="shopname" />
</ReferenceInput>
<TextInput source="ro_number" />
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
<AutocompleteInput
matchSuggestion={(filter, choice) =>
choice.ownr_fn &&
choice.ownr_fn.toLowerCase().includes(filter.toLowerCase())
}
optionText={(record) =>
`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
} (${record.ownr_ph1 || ""})`
}
/>
</ReferenceInput>
<ReferenceInput
label="Vehicle Id"
source="vehicleid"
reference="vehicles"
>
<SelectInput optionText="v_vin" />
</ReferenceInput>
<ReferenceInput label="Shopid" source="shopid" reference="bodyshops">
<SelectInput disabled optionText="shopname" />
</ReferenceInput>
<TextInput source="ro_number" />
<ReferenceInput label="Owner ID" source="ownerid" reference="owners">
<AutocompleteInput
matchSuggestion={(filter, choice) =>
choice.ownr_fn &&
choice.ownr_fn.toLowerCase().includes(filter.toLowerCase())
}
optionText={(record) =>
`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
} (${record.ownr_ph1 || ""})`
}
/>
</ReferenceInput>
<ReferenceInput
label="Vehicle Id"
source="vehicleid"
reference="vehicles"
>
<SelectInput optionText="v_vin" />
</ReferenceInput>
<BooleanInput source="inproduction" />
<BooleanInput source="converted" />
<TextInput disabled source="id" />
<TextInput disabled source="created_at" />
<TextInput disabled source="updated_at" />
</SimpleForm>
</FormTab>
<FormTab label="Labor Rates">
<NumberInput source="labor_rate_id" />
<NumberInput source="labor_rate_desc" />
<NumberInput source="rate_lab" />
<NumberInput source="rate_lad" />
<NumberInput source="rate_lae" />
<NumberInput source="rate_lar" />
<NumberInput source="rate_las" />
<NumberInput source="rate_laf" />
<NumberInput source="rate_lam" />
<NumberInput source="rate_lag" />
<NumberInput source="rate_atp" />
<NumberInput source="rate_lau" />
<NumberInput source="rate_la1" />
<NumberInput source="rate_la2" />
<NumberInput source="rate_la3" />
<NumberInput source="rate_la4" />
<NumberInput source="rate_mapa" />
<NumberInput source="rate_mash" />
<NumberInput source="rate_mahw" />
<NumberInput source="rate_ma2s" />
<NumberInput source="rate_ma3s" />
<NumberInput source="rate_ma2t" />
<NumberInput source="rate_mabl" />
<NumberInput source="rate_macs" />
<NumberInput source="rate_matd" />
<NumberInput source="federal_tax_rate" />
<NumberInput source="state_tax_rate" />
<NumberInput source="local_tax_rate" />
</FormTab>
<FormTab label="Dates">
<TextInput source="scheduled_in" />
<TextInput source="actual_in" />
<TextInput source="scheduled_completion" />
<TextInput source="actual_completion" />
<TextInput source="scheduled_delivery" />
<TextInput source="actual_delivery" />
<TextInput source="invoice_date" />
<TextInput source="date_estimated" />
<TextInput source="date_open" />
<TextInput source="date_scheduled" />
<TextInput source="date_invoiced" />
<TextInput source="date_exported" />
</FormTab>
<FormTab label="Insurance info">
<TextInput source="est_co_nm" />
<TextInput source="est_addr1" />
<TextInput source="est_addr2" />
<TextInput source="est_city" />
<TextInput source="est_st" />
<TextInput source="est_zip" />
<TextInput source="est_ctry" />
<TextInput source="est_ph1" />
<TextInput source="est_ea" />
<TextInput source="est_ct_ln" />
<TextInput source="est_ct_fn" />
<TextInput source="regie_number" />
<TextInput source="statusid" />
<TextInput source="ins_co_id" />
<TextInput source="ins_co_nm" />
<TextInput source="ins_addr1" />
<TextInput source="ins_addr2" />
<TextInput source="ins_city" />
<TextInput source="ins_st" />
<TextInput source="ins_zip" />
<TextInput source="ins_ctry" />
<TextInput source="ins_ph1" />
<TextInput source="ins_ph1x" />
<TextInput source="ins_ph2" />
<TextInput source="ins_ph2x" />
<TextInput source="ins_fax" />
<TextInput source="ins_faxx" />
<TextInput source="ins_ct_ln" />
<TextInput source="ins_ct_fn" />
<TextInput source="ins_title" />
<TextInput source="ins_ct_ph" />
<TextInput source="ins_ct_phx" />
<TextInput source="ins_ea" />
<TextInput source="ins_memo" />
<TextInput source="policy_no" />
<TextInput source="ded_amt" />
<TextInput source="ded_status" />
<TextInput source="asgn_no" />
<TextInput source="asgn_date" />
<TextInput source="asgn_type" />
<TextInput source="clm_no" />
<TextInput source="clm_ofc_id" />
<TextInput source="agt_co_id" />
<TextInput source="agt_co_nm" />
<TextInput source="agt_addr1" />
<TextInput source="agt_addr2" />
<TextInput source="agt_city" />
<TextInput source="agt_st" />
<TextInput source="agt_zip" />
<TextInput source="agt_ctry" />
<TextInput source="agt_ph1" />
<TextInput source="agt_ph1x" />
<TextInput source="agt_ph2" />
<TextInput source="agt_ph2x" />
<TextInput source="agt_fax" />
<TextInput source="agt_faxx" />
<TextInput source="agt_ct_ln" />
<TextInput source="agt_ct_fn" />
<TextInput source="agt_ct_ph" />
<TextInput source="agt_ct_phx" />
<TextInput source="agt_ea" />
<TextInput source="agt_lic_no" />
<TextInput source="loss_type" />
<TextInput source="loss_desc" />
<TextInput source="theft_ind" />
<TextInput source="cat_no" />
<TextInput source="tlos_ind" />
<TextInput source="ciecaid" />
<TextInput source="loss_date" />
<TextInput source="clm_ofc_nm" />
<TextInput source="clm_addr1" />
<TextInput source="clm_addr2" />
<TextInput source="clm_city" />
<TextInput source="clm_st" />
<TextInput source="clm_zip" />
<TextInput source="clm_ctry" />
<TextInput source="clm_ph1" />
<TextInput source="clm_ph1x" />
<TextInput source="clm_ph2" />
<TextInput source="clm_ph2x" />
<TextInput source="clm_fax" />
<TextInput source="clm_faxx" />
<TextInput source="clm_ct_ln" />
<TextInput source="clm_ct_fn" />
<TextInput source="clm_title" />
<TextInput source="clm_ct_ph" />
<TextInput source="clm_ct_phx" />
<TextInput source="clm_ea" />
<TextInput source="payee_nms" />
<TextInput source="pay_type" />
<TextInput source="pay_date" />
<TextInput source="pay_chknm" />
<TextInput source="pay_amt" />
</FormTab>
<FormTab label="Owner Data on Job">
<TextInput source="cust_pr" />
<TextInput source="insd_ln" />
<TextInput source="insd_fn" />
<TextInput source="insd_title" />
<TextInput source="insd_co_nm" />
<TextInput source="insd_addr1" />
<TextInput source="insd_addr2" />
<TextInput source="insd_city" />
<TextInput source="insd_st" />
<TextInput source="insd_zip" />
<TextInput source="insd_ctry" />
<TextInput source="insd_ph1" />
<TextInput source="insd_ph1x" />
<TextInput source="insd_ph2" />
<TextInput source="insd_ph2x" />
<TextInput source="insd_fax" />
<TextInput source="insd_faxx" />
<TextInput source="insd_ea" />
<TextInput source="ownr_ln" />
<TextInput source="ownr_fn" />
<TextInput source="ownr_title" />
<TextInput source="ownr_co_nm" />
<TextInput source="ownr_addr1" />
<TextInput source="ownr_addr2" />
<TextInput source="ownr_city" />
<TextInput source="ownr_st" />
<TextInput source="ownr_zip" />
<TextInput source="ownr_ctry" />
<TextInput source="ownr_ph1" />
<TextInput source="ownr_ph1x" />
<TextInput source="ownr_ph2" />
<TextInput source="ownr_ph2x" />
<TextInput source="ownr_fax" />
<TextInput source="ownr_faxx" />
<TextInput source="ownr_ea" />
</FormTab>
<FormTab label="Financial">
<TextInput source="clm_total" />
<TextInput source="owner_owing" />
</FormTab>
<FormTab label="Other">
<TextInput source="area_of_damage" />
<TextInput source="loss_cat" />
<TextInput source="special_coverage_policy" />
<TextInput source="csr" />
<TextInput source="po_number" />
<TextInput source="unit_number" />
<TextInput source="kmin" />
<TextInput source="kmout" />
<TextInput source="referral_source" />
<TextInput source="selling_dealer" />
<TextInput source="servicing_dealer" />
<TextInput source="servicing_dealer_contact" />
<TextInput source="selling_dealer_contact" />
<TextInput source="depreciation_taxes" />
<TextInput source="federal_tax_payable" />
<TextInput source="other_amount_payable" />
<TextInput source="towing_payable" />
<TextInput source="storage_payable" />
<TextInput source="adjustment_bottom_line" />
<TextInput source="tax_pstthr" />
<TextInput source="tax_tow_rt" />
<TextInput source="tax_sub_rt" />
<TextInput source="tax_paint_mat_rt" />
<TextInput source="tax_levies_rt" />
<TextInput source="tax_prethr" />
<TextInput source="tax_thramt" />
<TextInput source="tax_str_rt" />
<TextInput source="tax_lbr_rt" />
<TextInput source="adj_g_disc" />
<TextInput source="adj_towdis" />
<TextInput source="adj_strdis" />
<TextInput source="tax_predis" />
<TextInput source="rate_laa" />
<TextInput source="status" />
<TextInput source="cieca_stl" />
<TextInput source="g_bett_amt" />
<TextInput source="cieca_ttl" />
<TextInput source="plate_no" />
<TextInput source="plate_st" />
<TextInput source="v_vin" />
<TextInput source="v_model_yr" />
<TextInput source="v_model_desc" />
<TextInput source="v_make_desc" />
<TextInput source="v_color" />
<TextInput source="parts_tax_rates" />
<TextInput source="job_totals" />
<TextInput source="production_vars" />
<TextInput source="intakechecklist" />
<TextInput source="invoice_allocation" />
<TextInput source="kanbanparent" />
<TextInput source="employee_body" />
<TextInput source="employee_refinish" />
<TextInput source="employee_prep" />
</FormTab>
</TabbedForm>
</Edit>
);
export default JobsEdit;

View File

@@ -0,0 +1,69 @@
import { useQuery } from "@apollo/client";
import CircularProgress from "@material-ui/core/CircularProgress";
import React from "react";
import {
Datagrid,
Filter,
List,
ReferenceField,
SelectInput,
TextField,
TextInput,
} from "react-admin";
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
const JobsList = (props) => (
<List filters={<JobsFilter />} {...props}>
<Datagrid rowClick="edit">
<TextField source="id" label="Job ID" />
<ReferenceField source="shopid" reference="bodyshops" label="Shop Name">
<TextField source="shopname" />
</ReferenceField>
<TextField source="ro_number" label="RO Number" />
<TextField source="ownr_fn" label="Owner FN" />
<TextField source="ownr_ln" label="Owner LN" />
<TextField source="ownr_co_nm" label="Owner CO" />
<ReferenceField source="ownerid" reference="owners" label="Owner Record">
<TextField source="id" />
</ReferenceField>
<TextField source="v_model_yr" label="Year" />
<TextField source="v_make_desc" label="Make" />
<TextField source="v_model_desc" label="Model" />
<ReferenceField
source="vehicleid"
reference="vehicles"
label="Vehicle ID"
>
<TextField source="id" />
</ReferenceField>
</Datagrid>
</List>
);
const JobsFilter = (props) => {
const { loading, error, data } = useQuery(QUERY_ALL_SHOPS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (loading) return <CircularProgress />;
if (error) return JSON.stringify(error);
return (
<Filter {...props}>
<TextInput label="RO Number" source="ro_number" />
<TextInput label="Job ID" source="id" />
<SelectInput
source="shopid"
label="Bodyshop"
choices={data.bodyshops.map((b) => {
return { id: b.id, name: b.shopname };
})}
/>
</Filter>
);
};
export default JobsList;

View File

@@ -0,0 +1,280 @@
import React from "react";
import {
Datagrid,
EditButton,
NumberField,
ReferenceManyField,
Show,
Tab,
TabbedShowLayout,
TextField,
} from "react-admin";
const JobsShow = (props) => (
<Show {...props}>
<TabbedShowLayout>
<Tab label="summary">
<TextField source="id" />
<TextField source="created_at" />
<TextField source="updated_at" />
<TextField source="shopid" />
<TextField source="ro_number" />
<TextField source="ownerid" />
<TextField source="vehicleid" />
</Tab>
<Tab label="Job Lines">
<ReferenceManyField
reference="joblines"
target="jobid"
label="Job Lines"
>
<Datagrid>
<TextField source="id" />
<TextField source="line_ref" />
<TextField source="line_desc" />
<TextField source="line_ind" />
<NumberField source="db_price" />
<NumberField source="act_price" />
<NumberField source="part_qty" />
<NumberField source="mod_lb_hrs" />
<TextField source="mod_lbr_type" />
<TextField source="lbr_op" />
<EditButton />
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab label="other">
<TextField source="labor_rate_id" />
<TextField source="labor_rate_desc" />
<TextField source="rate_lab" />
<TextField source="rate_lad" />
<TextField source="rate_lae" />
<TextField source="rate_lar" />
<TextField source="rate_las" />
<TextField source="rate_laf" />
<TextField source="rate_lam" />
<TextField source="rate_lag" />
<TextField source="rate_atp" />
<TextField source="rate_lau" />
<TextField source="rate_la1" />
<TextField source="rate_la2" />
<TextField source="rate_la3" />
<TextField source="rate_la4" />
<TextField source="rate_mapa" />
<TextField source="rate_mash" />
<TextField source="rate_mahw" />
<TextField source="rate_ma2s" />
<TextField source="rate_ma3s" />
<TextField source="rate_ma2t" />
<TextField source="rate_mabl" />
<TextField source="rate_macs" />
<TextField source="rate_matd" />
<TextField source="federal_tax_rate" />
<TextField source="state_tax_rate" />
<TextField source="local_tax_rate" />
<TextField source="est_co_nm" />
<TextField source="est_addr1" />
<TextField source="est_addr2" />
<TextField source="est_city" />
<TextField source="est_st" />
<TextField source="est_zip" />
<TextField source="est_ctry" />
<TextField source="est_ph1" />
<TextField source="est_ea" />
<TextField source="est_ct_ln" />
<TextField source="est_ct_fn" />
<TextField source="scheduled_in" />
<TextField source="actual_in" />
<TextField source="scheduled_completion" />
<TextField source="actual_completion" />
<TextField source="scheduled_delivery" />
<TextField source="actual_delivery" />
<TextField source="regie_number" />
<TextField source="invoice_date" />
<TextField source="inproduction" />
<TextField source="statusid" />
<TextField source="ins_co_id" />
<TextField source="ins_co_nm" />
<TextField source="ins_addr1" />
<TextField source="ins_addr2" />
<TextField source="ins_city" />
<TextField source="ins_st" />
<TextField source="ins_zip" />
<TextField source="ins_ctry" />
<TextField source="ins_ph1" />
<TextField source="ins_ph1x" />
<TextField source="ins_ph2" />
<TextField source="ins_ph2x" />
<TextField source="ins_fax" />
<TextField source="ins_faxx" />
<TextField source="ins_ct_ln" />
<TextField source="ins_ct_fn" />
<TextField source="ins_title" />
<TextField source="ins_ct_ph" />
<TextField source="ins_ct_phx" />
<TextField source="ins_ea" />
<TextField source="ins_memo" />
<TextField source="policy_no" />
<TextField source="ded_amt" />
<TextField source="ded_status" />
<TextField source="asgn_no" />
<TextField source="asgn_date" />
<TextField source="asgn_type" />
<TextField source="clm_no" />
<TextField source="clm_ofc_id" />
<TextField source="date_estimated" />
<TextField source="date_open" />
<TextField source="date_scheduled" />
<TextField source="date_invoiced" />
<TextField source="date_exported" />
<TextField source="clm_total" />
<TextField source="owner_owing" />
<TextField source="converted" />
<TextField source="ciecaid" />
<TextField source="loss_date" />
<TextField source="clm_ofc_nm" />
<TextField source="clm_addr1" />
<TextField source="clm_addr2" />
<TextField source="clm_city" />
<TextField source="clm_st" />
<TextField source="clm_zip" />
<TextField source="clm_ctry" />
<TextField source="clm_ph1" />
<TextField source="clm_ph1x" />
<TextField source="clm_ph2" />
<TextField source="clm_ph2x" />
<TextField source="clm_fax" />
<TextField source="clm_faxx" />
<TextField source="clm_ct_ln" />
<TextField source="clm_ct_fn" />
<TextField source="clm_title" />
<TextField source="clm_ct_ph" />
<TextField source="clm_ct_phx" />
<TextField source="clm_ea" />
<TextField source="payee_nms" />
<TextField source="pay_type" />
<TextField source="pay_date" />
<TextField source="pay_chknm" />
<TextField source="pay_amt" />
<TextField source="agt_co_id" />
<TextField source="agt_co_nm" />
<TextField source="agt_addr1" />
<TextField source="agt_addr2" />
<TextField source="agt_city" />
<TextField source="agt_st" />
<TextField source="agt_zip" />
<TextField source="agt_ctry" />
<TextField source="agt_ph1" />
<TextField source="agt_ph1x" />
<TextField source="agt_ph2" />
<TextField source="agt_ph2x" />
<TextField source="agt_fax" />
<TextField source="agt_faxx" />
<TextField source="agt_ct_ln" />
<TextField source="agt_ct_fn" />
<TextField source="agt_ct_ph" />
<TextField source="agt_ct_phx" />
<TextField source="agt_ea" />
<TextField source="agt_lic_no" />
<TextField source="loss_type" />
<TextField source="loss_desc" />
<TextField source="theft_ind" />
<TextField source="cat_no" />
<TextField source="tlos_ind" />
<TextField source="cust_pr" />
<TextField source="insd_ln" />
<TextField source="insd_fn" />
<TextField source="insd_title" />
<TextField source="insd_co_nm" />
<TextField source="insd_addr1" />
<TextField source="insd_addr2" />
<TextField source="insd_city" />
<TextField source="insd_st" />
<TextField source="insd_zip" />
<TextField source="insd_ctry" />
<TextField source="insd_ph1" />
<TextField source="insd_ph1x" />
<TextField source="insd_ph2" />
<TextField source="insd_ph2x" />
<TextField source="insd_fax" />
<TextField source="insd_faxx" />
<TextField source="insd_ea" />
<TextField source="ownr_ln" />
<TextField source="ownr_fn" />
<TextField source="ownr_title" />
<TextField source="ownr_co_nm" />
<TextField source="ownr_addr1" />
<TextField source="ownr_addr2" />
<TextField source="ownr_city" />
<TextField source="ownr_st" />
<TextField source="ownr_zip" />
<TextField source="ownr_ctry" />
<TextField source="ownr_ph1" />
<TextField source="ownr_ph1x" />
<TextField source="ownr_ph2" />
<TextField source="ownr_ph2x" />
<TextField source="ownr_fax" />
<TextField source="ownr_faxx" />
<TextField source="ownr_ea" />
<TextField source="area_of_damage" />
<TextField source="loss_cat" />
<TextField source="special_coverage_policy" />
<TextField source="csr" />
<TextField source="po_number" />
<TextField source="unit_number" />
<TextField source="kmin" />
<TextField source="kmout" />
<TextField source="referral_source" />
<TextField source="selling_dealer" />
<TextField source="servicing_dealer" />
<TextField source="servicing_dealer_contact" />
<TextField source="selling_dealer_contact" />
<TextField source="depreciation_taxes" />
<TextField source="federal_tax_payable" />
<TextField source="other_amount_payable" />
<TextField source="towing_payable" />
<TextField source="storage_payable" />
<TextField source="adjustment_bottom_line" />
<TextField source="tax_pstthr" />
<TextField source="tax_tow_rt" />
<TextField source="tax_sub_rt" />
<TextField source="tax_paint_mat_rt" />
<TextField source="tax_levies_rt" />
<TextField source="tax_prethr" />
<TextField source="tax_thramt" />
<TextField source="tax_str_rt" />
<TextField source="tax_lbr_rt" />
<TextField source="adj_g_disc" />
<TextField source="adj_towdis" />
<TextField source="adj_strdis" />
<TextField source="tax_predis" />
<TextField source="rate_laa" />
<TextField source="status" />
<TextField source="cieca_stl" />
<TextField source="g_bett_amt" />
<TextField source="cieca_ttl" />
<TextField source="plate_no" />
<TextField source="plate_st" />
<TextField source="v_vin" />
<TextField source="v_model_yr" />
<TextField source="v_model_desc" />
<TextField source="v_make_desc" />
<TextField source="v_color" />
<TextField source="parts_tax_rates" />
<TextField source="job_totals" />
<TextField source="production_vars" />
<TextField source="intakechecklist" />
<TextField source="invoice_allocation" />
<TextField source="kanbanparent" />
<TextField source="employee_body" />
<TextField source="employee_refinish" />
<TextField source="employee_prep" />
</Tab>
</TabbedShowLayout>
</Show>
);
export default JobsShow;

View File

@@ -0,0 +1,31 @@
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
firebase.initializeApp(config);
export const auth = firebase.auth();
export const firestore = firebase.firestore();
export default firebase;
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
unsubscribe();
resolve(userAuth);
}, reject);
});
};
export const updateCurrentUser = (userDetails) => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
userAuth.updateProfile(userDetails).then((r) => {
unsubscribe();
resolve(userAuth);
});
}, reject);
});
};

View File

@@ -0,0 +1,10 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_SHOPS = gql`
query QUERY_ALL_SHOPS {
bodyshops {
id
shopname
}
}
`;

13
admin/src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
admin/src/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App/App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

141
admin/src/serviceWorker.js Normal file
View File

@@ -0,0 +1,141 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

5
admin/src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

13650
admin/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

View File

@@ -1,14 +0,0 @@
GENERATE_SOURCEMAP=true
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GA_CODE=231103507
REACT_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU","authDomain":"imex-prod.firebaseapp.com","databaseURL":"https://imex-prod.firebaseio.com","projectId":"imex-prod","storageBucket":"imex-prod.appspot.com","messagingSenderId":"253497221485","appId":"1:253497221485:web:3c81c483b94db84b227a64","measurementId":"G-NTWBKG2L0M"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop
REACT_APP_CLOUDINARY_API_KEY=473322739956866
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BMgZT1NZztW2DsJl8Mg2L04hgY9FzAg6b8fbzgNAfww2VDzH3VE63Ot9EaP_U7KWS2JT-7HPHaw0T_Tw_5vkZc8'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
REACT_APP_REPORTS_SERVER_URL=https://reports.imex.online
REACT_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk

View File

@@ -1,14 +0,0 @@
REACT_APP_GRAPHQL_ENDPOINT=https://db.test.bodyshop.app/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.test.bodyshop.app/v1/graphql
REACT_APP_GA_CODE=231099835
REACT_APP_FIREBASE_CONFIG={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"}
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop
REACT_APP_CLOUDINARY_API_KEY=473322739956866
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_IS_TEST=true
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

3
client/.gitignore vendored
View File

@@ -1,3 +0,0 @@
# Sentry Config File
.sentryclirc

View File

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

View File

@@ -1,68 +1,73 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const {convertLegacyToken} = require('@ant-design/compatible/lib');
const {theme} = require('antd/lib');
const {defaultAlgorithm, defaultSeed} = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = {
plugins: [
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
//
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
plugins: [
{
plugin: SentryWebpackPlugin,
options: {
// sentry-cli configuration
authToken:
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
org: "snapt-software",
project: "imexonline",
release: process.env.REACT_APP_GIT_SHA,
return item;
}),
},
};
},
// webpack-specific configuration
include: ".",
ignore: ["node_modules", "webpack.config.js"],
},
},
devtool: "source-map",
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
...(process.env.NODE_ENV === "development"
? { "@primary-color": "#a51d1d" }
: {
//"@primary-color": "#1DA57A"
}),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
// "@warning-color": "#faad14", // warning state color
// "@error-color": "#f5222d", // error state color
// "@font-size-base": "14px", // major text font size
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
// "@border-radius-base": "2px", // major border radius
// "@border-color-base": "#d9d9d9", // major border color
// "@box-shadow-base":
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => ({
...webpackConfig,
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
}),
},
}),
},
devtool: "source-map",
};

View File

@@ -1,17 +0,0 @@
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',
},
})

8
client/cypress.json Normal file
View File

@@ -0,0 +1,8 @@
{
"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
│ ├─ 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
├─ postcss-focus-open@4.0.0
├─ postcss-focus-visible@4.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
│ ├─ publisher: Jonathan Neal
│ ├─ email: jonathantneal@hotmail.com
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
├─ postcss-focus-within@3.0.0
│ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within

16334
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +1,103 @@
{
"name": "bodyshop",
"version": "0.2.1",
"engines": {
"node": "18.18.2"
},
"type": "commonjs",
"private": true,
"proxy": "http://localhost:4000",
"browser": {
"fs": false,
"path": false,
"os": false
},
"dependencies": {
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.9.0",
"@apollo/client": "^3.5.10",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.99.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.13.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"@sentry/react": "^6.19.6",
"@sentry/tracing": "^6.19.6",
"@splitsoftware/splitio-react": "^1.4.0",
"@stripe/react-stripe-js": "^1.7.1",
"@stripe/stripe-js": "^1.27.0",
"@tanem/react-nprogress": "^5.0.0",
"antd": "^4.19.5",
"apollo-link-logger": "^2.0.0",
"axios": "^0.26.1",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.1",
"dotenv": "^16.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"esbuild": "^0.20.0",
"exifr": "^7.1.3",
"firebase": "^10.7.2",
"graphql": "^16.6.0",
"i18next": "^23.8.1",
"i18next-browser-languagedetector": "^7.0.2",
"jsoneditor": "^10.0.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.54",
"logrocket": "^7.0.0",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"phone": "^3.1.42",
"firebase": "^9.6.10",
"graphql": "^16.3.0",
"i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.7.4",
"libphonenumber-js": "^1.9.51",
"logrocket": "^2.2.1",
"markerjs2": "^2.21.0",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"phone": "^3.1.15",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^8.1.0",
"query-string": "^7.1.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.2.0",
"react-big-calendar": "^1.8.7",
"react": "^18.0.0",
"react-big-calendar": "^0.40.0",
"react-color": "^2.19.3",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.1",
"react-icons": "^5.0.1",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^9.0.1",
"react-number-format": "^5.1.4",
"react-redux": "^9.1.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.21.2",
"react-cookie": "^4.1.1",
"react-dom": "^18.0.0",
"react-drag-listview": "^0.1.9",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.6",
"react-icons": "^4.3.1",
"react-number-format": "^4.9.1",
"react-redux": "^7.2.8",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5",
"recharts": "^2.11.0",
"redux": "^5.0.1",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.9",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.70.0",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.1.5",
"sass": "^1.50.0",
"socket.io-client": "^4.4.1",
"styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"vite-plugin-svgr": "^4.2.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.3",
"workbox-cacheable-response": "^6.5.3",
"workbox-core": "^6.5.3",
"workbox-expiration": "^6.5.3",
"workbox-google-analytics": "^6.5.3",
"workbox-navigation-preload": "^6.5.3",
"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"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite",
"build": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"eulaize": "node src/utils/eulaize.js",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
},
"eslintConfig": {
"extends": [
@@ -131,29 +122,12 @@
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.10.3",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.4",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
"@sentry/webpack-plugin": "^1.18.8",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.5.4",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-style-import": "^2.0.0"
"source-map-explorer": "^2.5.2"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" />
<meta name="description" content="ImEX Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="public/logo192.png" />
<link rel="apple-touch-icon" href="logo192.png" />
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
@@ -67,7 +67,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@@ -82,7 +82,5 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.js"></script>
</body>
</html>

View File

@@ -1,53 +1,44 @@
import {ApolloProvider} from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
import {ConfigProvider} from "antd";
import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import 'dayjs/locale/en';
import moment from "moment";
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 client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
moment.locale("en-US");
dayjs.locale("en");
export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,
key: "anon",
},
});
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon",
},
};
export const factory = SplitSdk(config);
export default function AppContainer() {
const { t } = useTranslation();
function AppContainer() {
const {t} = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{autoComplete: "new-password"}}
locale={enLocale}
theme={themeProvider}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", {label: "${label}"}),
},
}}
>
<GlobalLoadingBar/>
<SplitFactoryProvider factory={factory}>
<App/>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
return (
<ApolloProvider client={client}>
<ConfigProvider
//componentSize="small"
input={{ autoComplete: "new-password" }}
locale={enLocale}
form={{
validateMessages: {
// eslint-disable-next-line no-template-curly-in-string
required: t("general.validation.required", { label: "${label}" }),
},
}}
>
<GlobalLoadingBar />
<SplitFactory factory={factory}>
<App />
</SplitFactory>
</ConfigProvider>
</ApolloProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,156 +1,165 @@
import {useSplitClient} from "@splitsoftware/splitio-react";
import {Button, Result} from "antd";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, {lazy, Suspense, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Route, Routes} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import {setOnline} from "../redux/application/application.actions";
import {selectOnline} from "../redux/application/application.selectors";
import {checkUserSession} from "../redux/user/user.actions";
import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentEula: selectCurrentEula
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const { LogRocket_Tracking } = useTreatments(
["LogRocket_Tracking"],
{},
bodyshop && bodyshop.imexshopid
);
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
}
const onlineListener = (e) => {
setOnline(true);
}
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.imex.online'
) {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
handleBeta();
checkUserSession();
}, [checkUserSession, setOnline]);
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
if (currentEula && !currentUser.eulaIsAccepted) {
return <Eula/>
const { t } = useTranslation();
window.addEventListener("offline", function (e) {
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized) {
if (
process.env.NODE_ENV === "production" &&
LogRocket_Tracking.treatment === "on"
) {
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [currentUser.authorized, LogRocket_Tracking.treatment]);
// Any route that is not assigned and matched will default to the Landing Page component
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return (
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}>
<Routes>
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<Route path="/manage/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<ManagePage/>}/>
</Route>
<Route path="/tech/*"
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<TechPageContainer/>}/>
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route path="*" element={<DocumentEditorContainer/>}/>
</Route>
</Routes>
</Suspense>
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
return (
<Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
<ErrorBoundary>
<Route exact path="/" component={LandingPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/signin" component={SignInPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/resetpassword" component={ResetPassword} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary>
<ErrorBoundary>
<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>
</Switch>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,10 +1,6 @@
//Global Styles.
@import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;
@@ -146,17 +142,3 @@
}
}
}
.job-line-manual {
color: tomato;
font-style: italic;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;
}
.rowWithColor > td {
background-color: var(--bgColor) !important;
}

View File

@@ -1,60 +0,0 @@
import {defaultsDeep} from "lodash";
import {theme} from "antd";
const {defaultAlgorithm, darkAlgorithm} = theme;
let isDarkMode = false;
/**
* Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/
const defaultTheme = {
components: {
Table: {
rowHoverBg: '#e7f3ff',
rowSelectedBg: '#e6f7ff',
headerSortHoverBg: 'transparent',
},
Menu: {
darkItemHoverBg: '#1677ff',
itemHoverBg: '#1677ff',
horizontalItemHoverBg: '#1677ff',
}
},
token: {
colorPrimary: '#1677ff'
}
};
/**
* Development theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const devTheme = {
components: {
Menu: {
darkItemHoverBg: '#a51d1d',
itemHoverBg: '#a51d1d',
horizontalItemHoverBg: '#a51d1d',
}
},
token: {
colorPrimary: '#a51d1d'
}
};
/**
* Production theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme
: prodTheme;
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
}
export default finaltheme;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

View File

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

@@ -0,0 +1,96 @@
import {
PaymentRequestButtonElement,
useStripe,
} from "@stripe/react-stripe-js";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
function Test({ bodyshop, setEmailOptions }) {
const stripe = useStripe();
const [paymentRequest, setPaymentRequest] = useState(null);
useEffect(() => {
if (stripe) {
const pr = stripe.paymentRequest({
country: "CA",
displayItems: [{ label: "Deductible", amount: 1099 }],
currency: "cad",
total: {
label: "Demo total",
amount: 1099,
},
requestPayerName: true,
requestPayerEmail: true,
});
// Check the availability of the Payment Request API.
pr.canMakePayment().then((result) => {
if (result) {
setPaymentRequest(pr);
} else {
// var details = {
// total: { label: "", amount: { currency: "CAD", value: "0.00" } },
// };
new PaymentRequest(
[{ supportedMethods: ["basic-card"] }],
{}
// details
).show();
}
});
}
}, [stripe]);
if (paymentRequest) {
return (
<div style={{ height: "300px" }}>
<PaymentRequestButtonElement options={{ paymentRequest }} />
</div>
);
}
return (
<div>
<button
onClick={() => {
setEmailOptions({
messageOptions: {
to: ["patrickwf@gmail.com"],
replyTo: bodyshop.email,
},
template: {
name: TemplateList().parts_order.key,
variables: {
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
},
},
});
}}
>
send email
</button>
<button
onClick={() => {
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
}}
>
Log an ImEX Event.
</button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -1,63 +0,0 @@
{
"status": 24201299,
"custid": 19607899,
"paymentid": 24201299,
"response": "A",
"authcode": "498680",
"declinereason": "Approved",
"fee": 0,
"invoice": "",
"account": "john",
"amount": 1000,
"amountincludesfee": false,
"total": 1000,
"paymenttype": "C",
"methodhint": "VI ***1111",
"cardbrand": "Visa",
"cardnumdisplay": "***1111",
"receiptelements": {
"authcode": "498680",
"cust_srv_ph_num": "1-555-555-5555",
"rcpt_pg_ftr_txt": "Thank You\nPlease Come Again",
"rcpt_currency": "USD",
"responsecode": "A",
"rcpt_pay_mthd": "Visa",
"transid": "C00 915799",
"merch_disp_nm": "CP Devel Test",
"rcpt_input_mthd": "Keyed",
"rcpt_pg_hdr_txt": "Welcome!",
"rcpt_tran_time": "Thursday February 23 2023, 11:25:36 pm +08",
"rcpt_trans_type": "Normal Transaction (Sale)",
"message": "Approved",
"rcpt_dba_addr": "1234 Storefront Ave\nSome City, UT 84111",
"avsdata": "N",
"receiptrequirements": "S",
"rcpt_cardnum": "************1111",
"cv2result": "M",
"rfnd_policy_txt": "<b>No Refunds</b>\nStore Credit Only",
"labels": {
"tranref": "REF#",
"tid": "TID",
"validationcode": "ValCode",
"emvapplicationid": "AID",
"emvatc": "ATC",
"rcpt_pay_mthd": "Pay Method",
"transid": "TransID",
"rcpt_input_mthd": "IMode",
"emvtsi": "TSI",
"emvac": "AC",
"rcpt_trans_type": "TranType",
"emvapplicationname": "PApp",
"visarewards": "RewardsProg"
}
},
"receipttoken": "H4sIAAAAAAAAACXMTQuCMBgA4P/ynh3tw_3dBI/ipQ8NOtRN53QiblpBRfTfCzo/8LwhxGAdZCCwFYoJJFQjI2kvHdGu74lVkgmrWyWNhASW5jW7cB87yHjKKePGJODnxrrnMl7dDTKmEJlSOqV/_N30XPpyj2Eddq57_KKZ8FLzmh_G1VQnVfhjiXGK1XYTc/h8AVOkf4qUAAAA",
"call": "card_payment",
"nonce": "488b5568-b5c1-4f38-8b2f-3b050f3abb11P",
"hmac": "JyPAJ9Yx0SlYBTtqns1OxAFRt+xF3l2UiLPO5zTDRBE=",
"paymentreferenceid": "C19607899P24201299",
"cardnum": "...1111",
"email": "",
"nameOnCard": "John Allen",
"cardType": "visa"
}

View File

@@ -0,0 +1,9 @@
import React from "react";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
export default function Test() {
return (
<div>
<QboAuthorizeComponent />
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setRefundPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
});
function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal);
return (
<div>
<Button
onClick={() =>
setRefundPaymentContext({
context: {},
})
}
>
Open Modal
</Button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -13,9 +13,6 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
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 {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -30,12 +27,7 @@ export default connect(
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({
bodyshop,
loading,
bills,
refetch,
}) {
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -139,9 +131,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -150,13 +144,14 @@ export function AccountingPayablesTableComponent({
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
<div>
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
/>
</div>
),
},
];
@@ -182,19 +177,11 @@ export function AccountingPayablesTableComponent({
<Card
extra={
<Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />
@@ -211,7 +198,7 @@ export function AccountingPayablesTableComponent({
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }}
pagination={{ position: "top", pageSize: 50 }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -1,21 +1,18 @@
import { Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -34,7 +31,6 @@ export function AccountingPayablesTableComponent({
bodyshop,
loading,
payments,
refetch,
}) {
const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]);
@@ -108,6 +104,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "transactionid",
key: "transactionid",
},
{
title: t("payments.fields.stripeid"),
dataIndex: "stripeid",
key: "stripeid",
},
{
title: t("payments.fields.created_at"),
dataIndex: "created_at",
@@ -129,9 +130,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -145,7 +148,6 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
refetch={refetch}
/>
),
},
@@ -181,19 +183,11 @@ export function AccountingPayablesTableComponent({
<Card
extra={
<Space wrap>
<PaymentMarkSelectedExported
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />
@@ -210,7 +204,7 @@ export function AccountingPayablesTableComponent({
<Table
loading={loading}
dataSource={dataSource}
pagination={{ position: "top", pageSize: pageLimit }}
pagination={{ position: "top", pageSize: 50 }}
columns={columns}
rowKey="id"
onChange={handleTableChange}

View File

@@ -14,7 +14,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,7 +30,6 @@ export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
refetch,
}) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
@@ -141,9 +139,12 @@ export function AccountingReceivablesTableComponent({
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
@@ -156,7 +157,6 @@ export function AccountingReceivablesTableComponent({
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>
@@ -207,7 +207,6 @@ export function AccountingReceivablesTableComponent({
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (

View File

@@ -2,5 +2,5 @@ import { Alert } from "antd";
import React from "react";
export default function AlertComponent(props) {
return <Alert {...props} />;
return <Alert {...props} />;
}

View File

@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
);
return (
<Popover content={popContent} open={visibility}>
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -59,7 +59,7 @@ export default connect(
);
return (
<Popover content={popContent} open={visibility}>
<Popover content={popContent} visible={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>

View File

@@ -4,7 +4,6 @@ import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
import {pageLimit} from "../../utils/config";
export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
@@ -75,7 +74,7 @@ export default function AuditTrailListComponent({ loading, data }) {
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}

View File

@@ -4,8 +4,6 @@ import { useQuery } from "@apollo/client";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
import { Card, Row } from "antd";
export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
@@ -20,20 +18,10 @@ export default function AuditTrailListContainer({ recordId }) {
{error ? (
<AlertComponent type="error" message={error.message} />
) : (
<Row gutter={[16, 16]}>
<Card>
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
<Card>
<EmailAuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
</Row>
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : null}
/>
)}
</div>
);

View File

@@ -1,64 +0,0 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import {pageLimit} from "../../utils/config";
export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",
key: " created",
width: "10%",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created,
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
},
];
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
}

View File

@@ -1,135 +0,0 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
returnLoading,
returnData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
}
}, [returnData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid ||
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) {
return null;
}
if (returnLoading) return <Skeleton />;
return (
<Form.List name="outstanding_returns">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("bills.labels.creditsnotreceived")}
</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("parts_orders.fields.line_desc")}</th>
<th>{t("parts_orders.fields.part_type")}</th>
<th>{t("parts_orders.fields.quantity")}</th>
<th>{t("parts_orders.fields.act_price")}</th>
<th>{t("parts_orders.fields.cost")}</th>
<th>{t("parts_orders.labels.mark_as_received")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cm_received`}
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -1,19 +0,0 @@
.bill-cm-returns-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill, callback }) {
export default function BillDeleteButton({ bill }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
@@ -15,8 +15,7 @@ export default function BillDeleteButton({ bill, callback }) {
setLoading(true);
const result = await deleteBill({
variables: { billId: bill.id },
update(cache, { errors }) {
if (errors) return;
update(cache) {
cache.modify({
fields: {
bills(existingBills, { readField }) {
@@ -36,25 +35,12 @@ export default function BillDeleteButton({ bill, callback }) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
if (callback && typeof callback === "function") callback(bill.id);
} else {
//Check if it's an fkey violation.
const error = JSON.stringify(result.errors);
if (error.toLowerCase().includes("inventory_billid_fkey")) {
notification["error"]({
message: t("bills.errors.deleting", {
error: t("bills.errors.existinginventoryline"),
}),
});
} else {
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
notification["error"]({
message: t("bills.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);

View File

@@ -1,251 +0,0 @@
import {useMutation, useQuery} from "@apollo/client";
import {Button, Form, Popconfirm, Space} from "antd";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { 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 AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillPrintButton from "../bill-print-button/bill-print-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
import {PageHeader} from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
const search = queryString.parse(useLocation().search);
const {t} = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid},
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
// ... rest of the code remains the same
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setOpen(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const {billlines, upload, ...bill} = values;
const updates = [];
updates.push(
update_bill({
variables: {billId: search.billid, bill: bill},
})
);
billlines.forEach((l) => {
delete l.selected;
});
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({variables: {id: d.id}}));
});
billlines.forEach((billline) => {
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
{loading && <LoadingSkeleton />}
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm
open={open}
onConfirm={() => form.submit()}
onCancel={() => setOpen(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{id: data ? data.bills_by_pk.jobid : null}}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -1,185 +0,0 @@
import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const { t } = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
delete search.billid;
history({ search: queryString.stringify(search) });
setOpen(false);
};
useEffect(() => {
if (open === false) form.resetFields();
}, [open, form]);
return (
<>
<Modal
open={open}
onCancel={() => setOpen(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
});
}}
/>
</td>
<td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_price")}</td>
<td>{t("billlines.fields.actual_cost")}</td>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setOpen(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
}

View File

@@ -1,12 +1,65 @@
import { Drawer, Grid } from "antd";
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Drawer,
Form,
Grid,
PageHeader,
Popconfirm,
Space,
} from "antd";
import moment from "moment";
import queryString from "query-string";
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
export default function BillDetailEditcontainer() {
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
}) {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -17,24 +70,232 @@ export default function BillDetailEditcontainer() {
sm: "100%",
md: "100%",
lg: "100%",
xl: "90%",
xxl: "90%",
xl: "80%",
xxl: "80%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
useEffect(() => {
if (search.billid && data) {
form.resetFields();
}
}, [form, search.billid, data]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<Drawer
width={drawerPercentage}
onClose={() => {
delete search.billid;
history({ search: queryString.stringify(search) });
history.push({ search: queryString.stringify(search) });
}}
destroyOnClose
open={search.billid}
visible={search.billid}
>
<BillDetailEditComponent />
{loading && <LoadingSkeleton />}
{!loading && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<Button
disabled={data.bills_by_pk.is_credit_memo}
onClick={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
</Form>
</>
)}
</Drawer>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -1,18 +1,16 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
import { Button, Form, Modal, notification, Space } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -20,14 +18,10 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
@@ -37,12 +31,10 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
const Templates = TemplateList("job_special");
function BillEnterModalContainer({
billEnterModal,
toggleModalVisible,
@@ -55,14 +47,9 @@ function BillEnterModalContainer({
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage(
"enter_bill_generate_label",
false
);
const formValues = useMemo(() => {
return {
...billEnterModal.context.bill,
@@ -89,13 +76,7 @@ function BillEnterModalContainer({
}
setLoading(true);
const {
upload,
location,
outstanding_returns,
inventory,
...remainingValues
} = values;
const { upload, location, ...remainingValues } = values;
let adjustmentsToInsert = {};
@@ -126,24 +107,13 @@ function BillEnterModalContainer({
deductedfromlbr: deductedfromlbr,
lbr_adjustment,
joblineid: i.joblineid === "noline" ? null : i.joblineid,
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) ||
false,
state:
(i.applicable_taxes && i.applicable_taxes.state) ||
false,
local:
(i.applicable_taxes && i.applicable_taxes.local) ||
false,
},
};
}),
},
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
const adjKeys = Object.keys(adjustmentsToInsert);
@@ -163,14 +133,6 @@ function BillEnterModalContainer({
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
});
const jobUpdate = client.mutate({
@@ -188,25 +150,10 @@ function BillEnterModalContainer({
});
return;
}
}
const markPolReceived =
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(),
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
if (!!r1.errors) {
@@ -220,105 +167,52 @@ function BillEnterModalContainer({
}
const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed =
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId,
},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
//If it's not a credit memo, update the statuses.
if (!values.is_credit_memo) {
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
},
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
},
});
})
);
}
},
});
})
);
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => {
handleLocalUpload({
ev: { file: u.originFileObj },
context: {
jobid: values.jobid,
invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid,
},
});
});
} else {
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
///////////////////////////
setLoading(false);
notification["success"]({
message: t("bills.successes.created"),
});
if (generateLabel) {
GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billId,
},
},
{},
"p"
);
}
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
});
if (enterAgain) {
@@ -346,18 +240,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]);
useEffect(() => {
if (billEnterModal.open) {
if (billEnterModal.visible) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.open, form, formValues]);
}, [billEnterModal.visible, form, formValues]);
return (
<Modal
title={t("bills.labels.new")}
width={"98%"}
open={billEnterModal.open}
visible={billEnterModal.visible}
okText={t("general.actions.save")}
keyboard="false"
onOk={() => form.submit()}
@@ -368,12 +262,6 @@ function BillEnterModalContainer({
}}
footer={
<Space>
<Checkbox
checked={generateLabel}
onChange={(e) => setGenerateLabel(e.target.checked)}
>
{t("bills.labels.generatepartslabel")}
</Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}

View File

@@ -1,18 +1,25 @@
import Icon, {UploadOutlined} from "@ant-design/icons";
import {useApolloClient} from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
import dayjs from "../../utils/day";
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 Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { MdOpenInNew } from "react-icons/md";
import {
Alert,
Divider,
Form,
Input,
Select,
Space,
Statistic,
Switch,
Upload,
} from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
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 BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -20,307 +27,216 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import {CalculateBillTotal} from "./bill-form.totals.utility";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) {
const {t} = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({
attributes: {},
names: ["Extended_Bill_Posting", "ClosingPeriod"],
splitKey: bodyshop.imexshopid,
});
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
});
form.setFieldsValue({ billlines });
export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
};
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
useEffect(() => {
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
useEffect(() => {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
const vendorId = form.getFieldValue("vendorid");
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
if (form.getFieldValue("jobid")) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
}
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item
style={{ display: "none" }}
name="isinhouse"
valuePropName="checked"
>
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
}
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
jobId: jobId,
vendorId: vendorId,
invoice_number: value,
vendorid: vendorid,
},
});
}
}
});
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
<div>
<FormFieldsChanged form={form}/>
<Form.Item
style={{display: "none"}}
name="isinhouse"
valuePropName="checked"
>
<Switch/>
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew}/>
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber}/>
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
dayjs(value)
.startOf("day")
.isSameOrAfter(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
dayjs(value)
.startOf("day")
.isSameOrBefore(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({getFieldValue}) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
!bodyshop.bill_allow_post_to_closed &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
},
@@ -341,17 +257,15 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item
@@ -375,16 +289,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
>
<CurrencyInput min={0} />
</Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item
span={2}
label={t("bills.labels.federal_tax_exempt")}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
<Form.Item shouldUpdate span={13}>
<Form.Item shouldUpdate span={15}>
{() => {
const values = form.getFieldsValue([
"billlines",
@@ -402,7 +307,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div align="right">
<div>
<Space wrap>
<Statistic
title={t("bills.labels.subtotal")}
@@ -460,55 +365,54 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
)}
<Form.Item
name="upload"
label="Upload"
style={{display: billEdit ? "none" : null}}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined/>
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
<Form.Item
name="upload"
label="Upload"
style={{ display: billEdit ? "none" : null }}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
multiple={true}
name="logo"
beforeUpload={() => false}
listType="picture"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

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