13
.gitignore
vendored
13
.gitignore
vendored
@@ -7,12 +7,18 @@
|
||||
client/node_modules
|
||||
client/.pnp
|
||||
client.pnp.js
|
||||
admin/node_modules
|
||||
admin/.pnp
|
||||
admin.pnp.js
|
||||
# testing
|
||||
/coverage
|
||||
client/coverage
|
||||
admin/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
client/build
|
||||
admin/build
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -25,7 +31,12 @@ client/.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
client/npm-debug.log*
|
||||
client/yarn-debug.log*
|
||||
client/yarn-error.log*
|
||||
admin/npm-debug.log*
|
||||
admin/yarn-debug.log*
|
||||
admin/yarn-error.log*
|
||||
|
||||
#Firebase Ignore
|
||||
# Logs
|
||||
|
||||
64
.vscode/bodyshopsnippets.code-snippets
vendored
Normal file
64
.vscode/bodyshopsnippets.code-snippets
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
// Place your bodyshop workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Const T useTranslation": {
|
||||
"prefix": "ttt",
|
||||
"body": ["const { t } = useTranslation();"],
|
||||
"description": "Use Translation Destructing."
|
||||
},
|
||||
" useTranslation import": {
|
||||
"prefix": "tti",
|
||||
"body": ["import { useTranslation } from \"react-i18next\";"],
|
||||
"description": "Use Translation import."
|
||||
},
|
||||
"Redux Setup": {
|
||||
"prefix": "rdx",
|
||||
"body": [
|
||||
"import { connect } from \"react-redux\";",
|
||||
"import { createStructuredSelector } from \"reselect\";",
|
||||
"const mapStateToProps = createStructuredSelector({",
|
||||
" //currentUser: selectCurrentUser",
|
||||
"});",
|
||||
"const mapDispatchToProps = dispatch => ({",
|
||||
" //setUserLanguage: language => dispatch(setUserLanguage(language))",
|
||||
"});",
|
||||
"export default connect (mapStateToProps,mapDispatchToProps)();"
|
||||
],
|
||||
"description": "General Redux."
|
||||
},
|
||||
" Apollo Loading Error Handling import": {
|
||||
"prefix": "ale",
|
||||
"body": [
|
||||
"if (loading) return <LoadingSpinner />;",
|
||||
"if (error) return <AlertComponent message={error.message} type=\"error\" />;"
|
||||
],
|
||||
"description": "Apollo Loading Error Handling import."
|
||||
},
|
||||
"Log IMEX EVent Import": {
|
||||
"prefix": "liei",
|
||||
"body": [
|
||||
"import { logImEXEvent } from \"../../firebase/firebase.utils\"; "
|
||||
],
|
||||
"description": "Apollo Loading Error Handling import."
|
||||
},
|
||||
|
||||
"Log IMEX EVent": {
|
||||
"prefix": "lie",
|
||||
"body": ["logImEXEvent(\"EventName\", { prop: \"value\" });"],
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
30
.vscode/launch.json
vendored
30
.vscode/launch.json
vendored
@@ -1,13 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Chrome",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceRoot}/src"
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Chrome",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceRoot}/src"
|
||||
},
|
||||
{
|
||||
"name": "Yarn Dev Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeArgs": ["dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,3 +12,11 @@ npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
||||
|
||||
Migrating to Staging:
|
||||
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
|
||||
|
||||
NGROK TEsting:
|
||||
|
||||
./ngrok.exe http https://localhost:5000 -host-header="localhost:5000"
|
||||
|
||||
|
||||
Finding deadfiles - run from client directory
|
||||
npx deadfile ./src/index.js --exclude build templates
|
||||
|
||||
@@ -49,3 +49,6 @@
|
||||
|
||||
--\* Set the region for the shop.
|
||||
-Counter Record - type: ronum
|
||||
|
||||
|
||||
Create an in house vendor record and add it to the bodyshop record.
|
||||
@@ -1,11 +0,0 @@
|
||||
server.env
|
||||
|
||||
AWSAccessKeyId=AKIAJYNXY5KCA25PB2JA
|
||||
AWSSecretKey=iYO/navUhHuEXc6fMgfUh1y3VZY1hF6ISrUMZ4de
|
||||
Bucket=bodyshop-app-dev
|
||||
|
||||
|
||||
client.env
|
||||
REACT_APP_GRAPHQL_ENDPOINT=https://bodyshop-dev-db.herokuapp.com/v1/graphql
|
||||
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://bodyshop-dev-db.herokuapp.com/v1/graphql
|
||||
REACT_APP_GA_CODE=217352234
|
||||
24
_reference/SampleMetadata.md
Normal file
24
_reference/SampleMetadata.md
Normal file
File diff suppressed because one or more lines are too long
75
_reference/dropletSetup.md
Normal file
75
_reference/dropletSetup.md
Normal file
@@ -0,0 +1,75 @@
|
||||
**Create an SSH key for local computer**
|
||||
|
||||
ssh-keygen -t rsa -C "your_email@example.com"
|
||||
|
||||
Copy the new key to clipboard:
|
||||
* Windows: clip < id_rsa.pub
|
||||
* Linux: sudo apt-get install xclip
|
||||
xclip -sel clip < ~/.ssh/id_rsa.pub
|
||||
* Mac: pbcopy < ~/.ssh/id_rsa.pub
|
||||
* Manual Copy: cat ~/.ssh/id_rsa.pub
|
||||
|
||||
Add the SSH key to the drop creation screen.
|
||||
|
||||
1. Create a new user to replace root user
|
||||
1. # adduser imex
|
||||
2. # usermod -aG sudo imex
|
||||
3. # su - imex
|
||||
4. $ mkdir ~/.ssh
|
||||
5. $ chmod 700 ~/.ssh
|
||||
6. $ nano ~/.ssh/authorized_keys
|
||||
7. Add the copied SSH key and save.
|
||||
8. $ chmod 600 ~/.ssh/authorized_keys #Restrict access to authorized keys.
|
||||
2. Setup the Firewall
|
||||
1. $ sudo ufw allow OpenSSH.
|
||||
2. $ sudo ufw enable
|
||||
3. Add Nginx & Configure
|
||||
1. $ sudo apt-get update
|
||||
2. $ sudo apt-get install nginx
|
||||
3. $ sudo ufw allow 'Nginx Full'
|
||||
4. $ sudo ufw app list
|
||||
1. Nginx Full: Opens both port 80 (normal, unencrypted web traffic) and port 443 (TLS/SSL encrypted traffic)
|
||||
2. Nginx Http: Opens only port 80 (normal, unencrypted web traffic)
|
||||
3. Nginx Https: Opens only port 443 (TLS/SSL encrypted traffic)
|
||||
5. Should now be able to go to IP and see nginx responding with a blank page.
|
||||
6. Install NodeJs
|
||||
1. $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
2. $ sudo apt install nodejs
|
||||
3. $ node --version
|
||||
7. Clone Source Code
|
||||
1. $ git clone git@bitbucket.org:snaptsoft/bodyshop.git //Requires SSH setup.
|
||||
2. $ cd bodyshop && npm install //Install all server dependencies.
|
||||
8. Setup PM2
|
||||
1. $ npm install pm2 -g //Had to be run as root.
|
||||
2. $ pm2 start ecosystem.config.js
|
||||
3. $ pm2 startup ubuntu //Ensure it starts when server does.
|
||||
9. Alter Nginx config
|
||||
1. sudo nano /etc/nginx/sites-available/default
|
||||
2. //Add Appropriate server names to the file. www. and non-www.
|
||||
3. Add the following inside the location of the server block:
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
10. Install Certbot
|
||||
4. $ sudo add-apt-repository ppa:certbot/certbot //Potential issue on ubuntu 20.04
|
||||
5. $ sudo apt-get update
|
||||
6. $ sudo apt install python-certbot-nginx
|
||||
7. $ sudo nano /etc/nginx/sites-available/default
|
||||
8. Find the existing server_name line and replace the underscore with your domain name:
|
||||
...
|
||||
server_name example.com www.example.com;
|
||||
...
|
||||
9. $ sudo nginx -t //Verify syntax.
|
||||
10. $ sudo systemctl reload nginx
|
||||
11. Generate Certificate
|
||||
11. $ sudo certbot --nginx -d example.com -d www.example.com //Follow prompts.
|
||||
12. $ sudo certbot renew --dry-run //Dry run to test auto renewal.
|
||||
|
||||
|
||||
ADding Yarn
|
||||
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get install yarn
|
||||
16
_reference/firebase.md
Normal file
16
_reference/firebase.md
Normal file
@@ -0,0 +1,16 @@
|
||||
1. Create a new project
|
||||
2. Setup sign in methods to be user and email only.
|
||||
3. Update .env to include config.
|
||||
4. Setup the Firebase CLI
|
||||
1. cd to client firebase at server directory.
|
||||
2. ensure all dependencies installed
|
||||
1. $ npm install firebase-functions@latest firebase-admin@latest --save
|
||||
2. $ npm install -g firebase-tools
|
||||
3. $ firebase login //Login as needed.
|
||||
5. Set the current projct
|
||||
1. firebase use <projectname>
|
||||
6. Deploy the function
|
||||
1. $ firebase deploy --only functions
|
||||
7. Add the allowed domains.
|
||||
8. Update server variables including FIREBASE_ADMINSDK_JSON, FIREBASE_DATABASE_URL
|
||||
9. Create the firestore and copy the rules from dev for userinstances.
|
||||
68
admin/README.md
Normal file
68
admin/README.md
Normal 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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
|
||||
44
admin/package.json
Normal file
44
admin/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.0.2",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-logger": "^1.2.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"firebase": "^7.17.0",
|
||||
"graphql": "^15.3.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-hasura-graphql": "^0.1.12",
|
||||
"react": "^16.13.1",
|
||||
"react-admin": "^3.7.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "3.4.1"
|
||||
},
|
||||
"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
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
43
admin/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<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
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
BIN
admin/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
admin/public/manifest.json
Normal file
25
admin/public/manifest.json
Normal 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
3
admin/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
admin/src/App/App.css
Normal file
38
admin/src/App/App.css
Normal 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
12
admin/src/App/App.js
Normal 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;
|
||||
9
admin/src/App/App.test.js
Normal file
9
admin/src/App/App.test.js
Normal 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();
|
||||
});
|
||||
7
admin/src/Assets/logo.svg
Normal file
7
admin/src/Assets/logo.svg
Normal 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 |
275
admin/src/components/admin-root/admin-root.component.jsx
Normal file
275
admin/src/components/admin-root/admin-root.component.jsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import { ApolloClient, InMemoryCache, ApolloProvider } 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 { 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-BodyShopAppBySnaptSoftware!`,
|
||||
// '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-BodyShopAppBySnaptSoftware!`,
|
||||
// // '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>Loading</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<Admin dataProvider={dataProvider} authProvider={authProvider}>
|
||||
<Resource
|
||||
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;
|
||||
46
admin/src/components/auth-provider/auth-provider.js
Normal file
46
admin/src/components/auth-provider/auth-provider.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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(true);
|
||||
console.log("token", token);
|
||||
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) => {
|
||||
console.log("Check Auth", params);
|
||||
const user = await getCurrentUser();
|
||||
if (!!user) {
|
||||
console.log("AuthProvider => checkAuth => Authorized");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
console.log("AuthProvider => checkAuth => Unauthorized");
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
checkError: (error) => {
|
||||
console.log("Check error");
|
||||
return Promise.resolve();
|
||||
},
|
||||
getPermissions: (params) => {
|
||||
console.log("get permissions", params);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
export default authProvider;
|
||||
26
admin/src/components/joblines/joblines.create.jsx
Normal file
26
admin/src/components/joblines/joblines.create.jsx
Normal 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;
|
||||
73
admin/src/components/joblines/joblines.edit.jsx
Normal file
73
admin/src/components/joblines/joblines.edit.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Edit,
|
||||
EmailField,
|
||||
DateTimeInput,
|
||||
DateField,
|
||||
NumberInput,
|
||||
BooleanInput,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
} from "react-admin";
|
||||
import { number } from "prop-types";
|
||||
|
||||
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;
|
||||
29
admin/src/components/joblines/joblines.list.jsx
Normal file
29
admin/src/components/joblines/joblines.list.jsx
Normal 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;
|
||||
24
admin/src/components/joblines/joblines.show.jsx
Normal file
24
admin/src/components/joblines/joblines.show.jsx
Normal 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;
|
||||
17
admin/src/components/jobs/jobs.create.jsx
Normal file
17
admin/src/components/jobs/jobs.create.jsx
Normal 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="est_number" />
|
||||
<TextInput source="ownr_fn" />
|
||||
<TextInput source="ownr_ln" />
|
||||
<TextInput source="converted" />
|
||||
<EmailField source="ownr_ea" />
|
||||
</SimpleForm>
|
||||
</Create>
|
||||
);
|
||||
|
||||
export default JobsCreate;
|
||||
255
admin/src/components/jobs/jobs.edit.jsx
Normal file
255
admin/src/components/jobs/jobs.edit.jsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React from "react";
|
||||
import { Edit, SimpleForm, TextInput } from "react-admin";
|
||||
|
||||
const JobsEdit = (props) => (
|
||||
<Edit {...props}>
|
||||
<SimpleForm margin="normal" variant="standard">
|
||||
<div
|
||||
style={{
|
||||
columns: "3 auto",
|
||||
// display: "flex",
|
||||
width: "100%",
|
||||
// justifyContent: "space-around",
|
||||
}}
|
||||
>
|
||||
<TextInput fullWidth source="id" />
|
||||
<TextInput fullWidth source="created_at" />
|
||||
<TextInput fullWidth source="updated_at" />
|
||||
|
||||
<TextInput fullWidth source="shopid" disabled />
|
||||
<TextInput fullWidth source="ro_number" />
|
||||
<TextInput fullWidth source="ownerid" />
|
||||
<TextInput fullWidth source="vehicleid" />
|
||||
<TextInput fullWidth source="labor_rate_id" />
|
||||
<TextInput fullWidth source="labor_rate_desc" />
|
||||
<TextInput fullWidth source="rate_lab" />
|
||||
<TextInput fullWidth source="rate_lad" />
|
||||
<TextInput fullWidth source="rate_lae" />
|
||||
<TextInput fullWidth source="rate_lar" />
|
||||
<TextInput fullWidth source="rate_las" />
|
||||
<TextInput fullWidth source="rate_laf" />
|
||||
<TextInput fullWidth source="rate_lam" />
|
||||
<TextInput fullWidth source="rate_lag" />
|
||||
<TextInput fullWidth source="rate_atp" />
|
||||
<TextInput fullWidth source="rate_lau" />
|
||||
<TextInput fullWidth source="rate_la1" />
|
||||
<TextInput fullWidth source="rate_la2" />
|
||||
<TextInput fullWidth source="rate_la3" />
|
||||
<TextInput fullWidth source="rate_la4" />
|
||||
<TextInput fullWidth source="rate_mapa" />
|
||||
<TextInput fullWidth source="rate_mash" />
|
||||
<TextInput fullWidth source="rate_mahw" />
|
||||
<TextInput fullWidth source="rate_ma2s" />
|
||||
<TextInput fullWidth source="rate_ma3s" />
|
||||
<TextInput fullWidth source="rate_ma2t" />
|
||||
<TextInput fullWidth source="rate_mabl" />
|
||||
<TextInput fullWidth source="rate_macs" />
|
||||
<TextInput fullWidth source="rate_matd" />
|
||||
<TextInput fullWidth source="federal_tax_rate" />
|
||||
<TextInput fullWidth source="state_tax_rate" />
|
||||
<TextInput fullWidth source="local_tax_rate" />
|
||||
<TextInput fullWidth source="est_co_nm" />
|
||||
<TextInput fullWidth source="est_addr1" />
|
||||
<TextInput fullWidth source="est_addr2" />
|
||||
<TextInput fullWidth source="est_city" />
|
||||
<TextInput fullWidth source="est_st" />
|
||||
<TextInput fullWidth source="est_zip" />
|
||||
<TextInput fullWidth source="est_ctry" />
|
||||
<TextInput fullWidth source="est_ph1" />
|
||||
<TextInput fullWidth source="est_ea" />
|
||||
<TextInput fullWidth source="est_ct_ln" />
|
||||
<TextInput fullWidth source="est_ct_fn" />
|
||||
<TextInput fullWidth source="scheduled_in" />
|
||||
<TextInput fullWidth source="actual_in" />
|
||||
<TextInput fullWidth source="scheduled_completion" />
|
||||
<TextInput fullWidth source="actual_completion" />
|
||||
<TextInput fullWidth source="scheduled_delivery" />
|
||||
<TextInput fullWidth source="actual_delivery" />
|
||||
<TextInput fullWidth source="regie_number" />
|
||||
<TextInput fullWidth source="invoice_date" />
|
||||
<TextInput fullWidth source="inproduction" />
|
||||
<TextInput fullWidth source="statusid" />
|
||||
<TextInput fullWidth source="ins_co_id" />
|
||||
<TextInput fullWidth source="ins_co_nm" />
|
||||
<TextInput fullWidth source="ins_addr1" />
|
||||
<TextInput fullWidth source="ins_addr2" />
|
||||
<TextInput fullWidth source="ins_city" />
|
||||
<TextInput fullWidth source="ins_st" />
|
||||
<TextInput fullWidth source="ins_zip" />
|
||||
<TextInput fullWidth source="ins_ctry" />
|
||||
<TextInput fullWidth source="ins_ph1" />
|
||||
<TextInput fullWidth source="ins_ph1x" />
|
||||
<TextInput fullWidth source="ins_ph2" />
|
||||
<TextInput fullWidth source="ins_ph2x" />
|
||||
<TextInput fullWidth source="ins_fax" />
|
||||
<TextInput fullWidth source="ins_faxx" />
|
||||
<TextInput fullWidth source="ins_ct_ln" />
|
||||
<TextInput fullWidth source="ins_ct_fn" />
|
||||
<TextInput fullWidth source="ins_title" />
|
||||
<TextInput fullWidth source="ins_ct_ph" />
|
||||
<TextInput fullWidth source="ins_ct_phx" />
|
||||
<TextInput fullWidth source="ins_ea" />
|
||||
<TextInput fullWidth source="ins_memo" />
|
||||
<TextInput fullWidth source="policy_no" />
|
||||
<TextInput fullWidth source="ded_amt" />
|
||||
<TextInput fullWidth source="ded_status" />
|
||||
<TextInput fullWidth source="asgn_no" />
|
||||
<TextInput fullWidth source="asgn_date" />
|
||||
<TextInput fullWidth source="asgn_type" />
|
||||
<TextInput fullWidth source="clm_no" />
|
||||
<TextInput fullWidth source="clm_ofc_id" />
|
||||
<TextInput fullWidth source="date_estimated" />
|
||||
<TextInput fullWidth source="date_open" />
|
||||
<TextInput fullWidth source="date_scheduled" />
|
||||
<TextInput fullWidth source="date_invoiced" />
|
||||
<TextInput fullWidth source="date_closed" />
|
||||
<TextInput fullWidth source="date_exported" />
|
||||
<TextInput fullWidth source="clm_total" />
|
||||
<TextInput fullWidth source="owner_owing" />
|
||||
<TextInput fullWidth source="converted" />
|
||||
<TextInput fullWidth source="ciecaid" />
|
||||
<TextInput fullWidth source="loss_date" />
|
||||
<TextInput fullWidth source="clm_ofc_nm" />
|
||||
<TextInput fullWidth source="clm_addr1" />
|
||||
<TextInput fullWidth source="clm_addr2" />
|
||||
<TextInput fullWidth source="clm_city" />
|
||||
<TextInput fullWidth source="clm_st" />
|
||||
<TextInput fullWidth source="clm_zip" />
|
||||
<TextInput fullWidth source="clm_ctry" />
|
||||
<TextInput fullWidth source="clm_ph1" />
|
||||
<TextInput fullWidth source="clm_ph1x" />
|
||||
<TextInput fullWidth source="clm_ph2" />
|
||||
<TextInput fullWidth source="clm_ph2x" />
|
||||
<TextInput fullWidth source="clm_fax" />
|
||||
<TextInput fullWidth source="clm_faxx" />
|
||||
<TextInput fullWidth source="clm_ct_ln" />
|
||||
<TextInput fullWidth source="clm_ct_fn" />
|
||||
<TextInput fullWidth source="clm_title" />
|
||||
<TextInput fullWidth source="clm_ct_ph" />
|
||||
<TextInput fullWidth source="clm_ct_phx" />
|
||||
<TextInput fullWidth source="clm_ea" />
|
||||
<TextInput fullWidth source="payee_nms" />
|
||||
<TextInput fullWidth source="pay_type" />
|
||||
<TextInput fullWidth source="pay_date" />
|
||||
<TextInput fullWidth source="pay_chknm" />
|
||||
<TextInput fullWidth source="pay_amt" />
|
||||
<TextInput fullWidth source="agt_co_id" />
|
||||
<TextInput fullWidth source="agt_co_nm" />
|
||||
<TextInput fullWidth source="agt_addr1" />
|
||||
<TextInput fullWidth source="agt_addr2" />
|
||||
<TextInput fullWidth source="agt_city" />
|
||||
<TextInput fullWidth source="agt_st" />
|
||||
<TextInput fullWidth source="agt_zip" />
|
||||
<TextInput fullWidth source="agt_ctry" />
|
||||
<TextInput fullWidth source="agt_ph1" />
|
||||
<TextInput fullWidth source="agt_ph1x" />
|
||||
<TextInput fullWidth source="agt_ph2" />
|
||||
<TextInput fullWidth source="agt_ph2x" />
|
||||
<TextInput fullWidth source="agt_fax" />
|
||||
<TextInput fullWidth source="agt_faxx" />
|
||||
<TextInput fullWidth source="agt_ct_ln" />
|
||||
<TextInput fullWidth source="agt_ct_fn" />
|
||||
<TextInput fullWidth source="agt_ct_ph" />
|
||||
<TextInput fullWidth source="agt_ct_phx" />
|
||||
<TextInput fullWidth source="agt_ea" />
|
||||
<TextInput fullWidth source="agt_lic_no" />
|
||||
<TextInput fullWidth source="loss_type" />
|
||||
<TextInput fullWidth source="loss_desc" />
|
||||
<TextInput fullWidth source="theft_ind" />
|
||||
<TextInput fullWidth source="cat_no" />
|
||||
<TextInput fullWidth source="tlos_ind" />
|
||||
<TextInput fullWidth source="cust_pr" />
|
||||
<TextInput fullWidth source="insd_ln" />
|
||||
<TextInput fullWidth source="insd_fn" />
|
||||
<TextInput fullWidth source="insd_title" />
|
||||
<TextInput fullWidth source="insd_co_nm" />
|
||||
<TextInput fullWidth source="insd_addr1" />
|
||||
<TextInput fullWidth source="insd_addr2" />
|
||||
<TextInput fullWidth source="insd_city" />
|
||||
<TextInput fullWidth source="insd_st" />
|
||||
<TextInput fullWidth source="insd_zip" />
|
||||
<TextInput fullWidth source="insd_ctry" />
|
||||
<TextInput fullWidth source="insd_ph1" />
|
||||
<TextInput fullWidth source="insd_ph1x" />
|
||||
<TextInput fullWidth source="insd_ph2" />
|
||||
<TextInput fullWidth source="insd_ph2x" />
|
||||
<TextInput fullWidth source="insd_fax" />
|
||||
<TextInput fullWidth source="insd_faxx" />
|
||||
<TextInput fullWidth source="insd_ea" />
|
||||
<TextInput fullWidth source="ownr_ln" />
|
||||
<TextInput fullWidth source="ownr_fn" />
|
||||
<TextInput fullWidth source="ownr_title" />
|
||||
<TextInput fullWidth source="ownr_co_nm" />
|
||||
<TextInput fullWidth source="ownr_addr1" />
|
||||
<TextInput fullWidth source="ownr_addr2" />
|
||||
<TextInput fullWidth source="ownr_city" />
|
||||
<TextInput fullWidth source="ownr_st" />
|
||||
<TextInput fullWidth source="ownr_zip" />
|
||||
<TextInput fullWidth source="ownr_ctry" />
|
||||
<TextInput fullWidth source="ownr_ph1" />
|
||||
<TextInput fullWidth source="ownr_ph1x" />
|
||||
<TextInput fullWidth source="ownr_ph2" />
|
||||
<TextInput fullWidth source="ownr_ph2x" />
|
||||
<TextInput fullWidth source="ownr_fax" />
|
||||
<TextInput fullWidth source="ownr_faxx" />
|
||||
<TextInput fullWidth source="ownr_ea" />
|
||||
<TextInput fullWidth source="area_of_damage" />
|
||||
<TextInput fullWidth source="loss_cat" />
|
||||
<TextInput fullWidth source="est_number" />
|
||||
<TextInput fullWidth source="special_coverage_policy" />
|
||||
<TextInput fullWidth source="csr" />
|
||||
<TextInput fullWidth source="po_number" />
|
||||
<TextInput fullWidth source="unit_number" />
|
||||
<TextInput fullWidth source="kmin" />
|
||||
<TextInput fullWidth source="kmout" />
|
||||
<TextInput fullWidth source="referral_source" />
|
||||
<TextInput fullWidth source="selling_dealer" />
|
||||
<TextInput fullWidth source="servicing_dealer" />
|
||||
<TextInput fullWidth source="servicing_dealer_contact" />
|
||||
<TextInput fullWidth source="selling_dealer_contact" />
|
||||
<TextInput fullWidth source="depreciation_taxes" />
|
||||
<TextInput fullWidth source="federal_tax_payable" />
|
||||
<TextInput fullWidth source="other_amount_payable" />
|
||||
<TextInput fullWidth source="towing_payable" />
|
||||
<TextInput fullWidth source="storage_payable" />
|
||||
<TextInput fullWidth source="adjustment_bottom_line" />
|
||||
<TextInput fullWidth source="tax_pstthr" />
|
||||
<TextInput fullWidth source="tax_tow_rt" />
|
||||
<TextInput fullWidth source="tax_sub_rt" />
|
||||
<TextInput fullWidth source="tax_paint_mat_rt" />
|
||||
<TextInput fullWidth source="tax_levies_rt" />
|
||||
<TextInput fullWidth source="tax_prethr" />
|
||||
<TextInput fullWidth source="tax_thramt" />
|
||||
<TextInput fullWidth source="tax_str_rt" />
|
||||
<TextInput fullWidth source="tax_lbr_rt" />
|
||||
<TextInput fullWidth source="adj_g_disc" />
|
||||
<TextInput fullWidth source="adj_towdis" />
|
||||
<TextInput fullWidth source="adj_strdis" />
|
||||
<TextInput fullWidth source="tax_predis" />
|
||||
<TextInput fullWidth source="rate_laa" />
|
||||
<TextInput fullWidth source="status" />
|
||||
<TextInput fullWidth source="cieca_stl" />
|
||||
<TextInput fullWidth source="g_bett_amt" />
|
||||
<TextInput fullWidth source="cieca_ttl" />
|
||||
<TextInput fullWidth source="plate_no" />
|
||||
<TextInput fullWidth source="plate_st" />
|
||||
<TextInput fullWidth source="v_vin" />
|
||||
<TextInput fullWidth source="v_model_yr" />
|
||||
<TextInput fullWidth source="v_model_desc" />
|
||||
<TextInput fullWidth source="v_make_desc" />
|
||||
<TextInput fullWidth source="v_color" />
|
||||
<TextInput fullWidth source="parts_tax_rates" />
|
||||
<TextInput fullWidth source="job_totals" />
|
||||
<TextInput fullWidth source="production_vars" />
|
||||
<TextInput fullWidth source="intakechecklist" />
|
||||
<TextInput fullWidth source="invoice_allocation" />
|
||||
<TextInput fullWidth source="kanbanparent" />
|
||||
<TextInput fullWidth source="employee_body" />
|
||||
<TextInput fullWidth source="employee_refinish" />
|
||||
<TextInput fullWidth source="employee_prep" />
|
||||
</div>
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
);
|
||||
|
||||
export default JobsEdit;
|
||||
62
admin/src/components/jobs/jobs.list.jsx
Normal file
62
admin/src/components/jobs/jobs.list.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Datagrid,
|
||||
Filter,
|
||||
List,
|
||||
ReferenceField,
|
||||
TextField,
|
||||
SelectInput,
|
||||
TextInput,
|
||||
} from "react-admin";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
|
||||
const JobsList = (props) => (
|
||||
<List filters={<JobsFilter />} {...props}>
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="id" />
|
||||
<ReferenceField source="shopid" reference="bodyshops">
|
||||
<TextField source="shopname" />
|
||||
</ReferenceField>
|
||||
<TextField source="ro_number" />
|
||||
<TextField source="est_number" />
|
||||
<TextField source="ownr_fn" />
|
||||
<TextField source="ownr_ln" />
|
||||
<TextField source="ownr_co_nm" />
|
||||
|
||||
<ReferenceField source="ownerid" reference="owners">
|
||||
<TextField source="id" />
|
||||
</ReferenceField>
|
||||
<TextField source="v_model_yr" />
|
||||
<TextField source="v_make_desc" />
|
||||
<TextField source="v_model_desc" />
|
||||
|
||||
<ReferenceField source="vehicleid" reference="vehicles">
|
||||
<TextField source="id" />
|
||||
</ReferenceField>
|
||||
</Datagrid>
|
||||
</List>
|
||||
);
|
||||
|
||||
const JobsFilter = (props) => {
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_SHOPS);
|
||||
if (loading) return <CircularProgress />;
|
||||
if (error) return JSON.stringify(error);
|
||||
|
||||
return (
|
||||
<Filter {...props}>
|
||||
<TextInput label="RO Number" source="ro_number" />
|
||||
<SelectInput
|
||||
source="shopid"
|
||||
choices={data.bodyshops.map((b) => {
|
||||
return { id: b.id, name: b.shopname };
|
||||
})}
|
||||
alwaysOn
|
||||
allowEmpty={false}
|
||||
/>
|
||||
</Filter>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobsList;
|
||||
283
admin/src/components/jobs/jobs.show.jsx
Normal file
283
admin/src/components/jobs/jobs.show.jsx
Normal file
@@ -0,0 +1,283 @@
|
||||
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_closed" />
|
||||
<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="est_number" />
|
||||
<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;
|
||||
31
admin/src/firebase/admin-firebase-utils.js
Normal file
31
admin/src/firebase/admin-firebase-utils.js
Normal 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);
|
||||
});
|
||||
};
|
||||
10
admin/src/graphql/admin.shop.queries.js
Normal file
10
admin/src/graphql/admin.shop.queries.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const QUERY_ALL_SHOPS = gql`
|
||||
query QUERY_ALL_SHOPS {
|
||||
bodyshops {
|
||||
id
|
||||
shopname
|
||||
}
|
||||
}
|
||||
`;
|
||||
13
admin/src/index.css
Normal file
13
admin/src/index.css
Normal 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
17
admin/src/index.js
Normal 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
141
admin/src/serviceWorker.js
Normal 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
5
admin/src/setupTests.js
Normal 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';
|
||||
12234
admin/yarn.lock
Normal file
12234
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
@@ -1,49 +1,62 @@
|
||||
{
|
||||
"name": "bodyshop",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0001",
|
||||
"private": true,
|
||||
"proxy": "https://localhost:5000",
|
||||
"dependencies": {
|
||||
"@nivo/pie": "^0.61.1",
|
||||
"@tinymce/tinymce-react": "^3.5.0",
|
||||
"aamva": "^1.2.0",
|
||||
"antd": "^4.1.0",
|
||||
"apollo-boost": "^0.4.4",
|
||||
"apollo-link-context": "^1.0.19",
|
||||
"apollo-link-error": "^1.1.12",
|
||||
"@lourenci/react-kanban": "^2.0.0",
|
||||
"@stripe/react-stripe-js": "^1.1.2",
|
||||
"@stripe/stripe-js": "^1.8.0",
|
||||
"@tanem/react-nprogress": "^3.0.34",
|
||||
"@tinymce/tinymce-react": "^3.6.0",
|
||||
"antd": "^4.4.2",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-error": "^1.1.13",
|
||||
"apollo-link-logger": "^1.2.3",
|
||||
"apollo-link-retry": "^2.2.15",
|
||||
"apollo-link-ws": "^1.0.19",
|
||||
"apollo-link-retry": "^2.2.16",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"axios": "^0.19.2",
|
||||
"dinero.js": "^1.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"firebase": "^7.13.1",
|
||||
"graphql": "^14.6.0",
|
||||
"i18next": "^19.3.4",
|
||||
"i18next-browser-languagedetector": "^4.1.1",
|
||||
"node-sass": "^4.13.1",
|
||||
"query-string": "^6.11.1",
|
||||
"fingerprintjs2": "^2.1.0",
|
||||
"firebase": "^7.16.0",
|
||||
"graphql": "^15.3.0",
|
||||
"i18next": "^19.6.0",
|
||||
"i18next-browser-languagedetector": "^5.0.0",
|
||||
"logrocket": "^1.0.9",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"phone": "^2.4.13",
|
||||
"query-string": "^6.13.1",
|
||||
"react": "^16.13.1",
|
||||
"react-apollo": "^3.1.3",
|
||||
"react-apollo": "^3.1.5",
|
||||
"react-barcode": "^1.4.0",
|
||||
"react-big-calendar": "^0.24.1",
|
||||
"react-big-calendar": "^0.26.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-drag-listview": "^0.1.7",
|
||||
"react-ga": "^3.1.2",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^0.18.3",
|
||||
"react-i18next": "^11.3.4",
|
||||
"react-icons": "^3.9.0",
|
||||
"react-image-file-resizer": "^0.2.1",
|
||||
"react-i18next": "^11.7.0",
|
||||
"react-icons": "^3.10.0",
|
||||
"react-image-file-resizer": "^0.3.1",
|
||||
"react-moment": "^0.9.7",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-resizable": "^1.10.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-trello": "^2.2.7",
|
||||
"react-virtualized": "^9.21.2",
|
||||
"recharts": "^1.8.5",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-state-sync": "^3.1.1",
|
||||
"reselect": "^4.0.0",
|
||||
"styled-components": "^5.0.1",
|
||||
"subscriptions-transport-ws": "^0.9.16"
|
||||
"styled-components": "^5.1.1",
|
||||
"subscriptions-transport-ws": "^0.9.17"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
@@ -68,7 +81,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/react-testing": "^3.1.3",
|
||||
"@apollo/react-testing": "^3.1.4",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
53
client/public/firebase-messaging-sw.js
Normal file
53
client/public/firebase-messaging-sw.js
Normal file
@@ -0,0 +1,53 @@
|
||||
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js");
|
||||
importScripts(
|
||||
"https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js"
|
||||
);
|
||||
|
||||
firebase.initializeApp({
|
||||
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",
|
||||
});
|
||||
|
||||
// firebase.initializeApp({
|
||||
// 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",
|
||||
// });
|
||||
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
self.addEventListener("fetch", (fetch) => {
|
||||
//required for installation as a PWA. Can ignore for now.
|
||||
//console.log("fetch", fetch);
|
||||
});
|
||||
|
||||
messaging.setBackgroundMessageHandler(function (payload) {
|
||||
return self.registration.showNotification(
|
||||
"[SW]" + payload.notification.title,
|
||||
payload.notification
|
||||
);
|
||||
});
|
||||
|
||||
//Handles the notification getting clicked.
|
||||
self.addEventListener("notificationclick", function (event) {
|
||||
console.log("SW notificationclick", event);
|
||||
// event.notification.close();
|
||||
if (event.action === "archive") {
|
||||
// Archive action was clicked
|
||||
archiveEmail();
|
||||
} else {
|
||||
// Main body of notification was clicked
|
||||
clients.openWindow("/inbox");
|
||||
}
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
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>BodyShop | ImEX Systems Inc.</title>
|
||||
<title>ImEX Online</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"short_name": "Bodyshop.app",
|
||||
"name": "Bodyshop Management System",
|
||||
"description": "The ultimate bodyshop management system",
|
||||
"short_name": "ImEX Online",
|
||||
"name": "ImEX Online",
|
||||
"description": "The ultimate bodyshop management system.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
@@ -22,5 +22,6 @@
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#fff",
|
||||
"background_color": "#fff"
|
||||
"background_color": "#fff",
|
||||
"gcm_sender_id": "103953800507"
|
||||
}
|
||||
|
||||
@@ -9,117 +9,134 @@ import apolloLogger from "apollo-link-logger";
|
||||
import { RetryLink } from "apollo-link-retry";
|
||||
import { WebSocketLink } from "apollo-link-ws";
|
||||
import { getMainDefinition } from "apollo-utilities";
|
||||
import React, { Component } from "react";
|
||||
import LogRocket from "logrocket";
|
||||
import React from "react";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import errorLink from "../graphql/apollo-error-handling";
|
||||
import App from "./App";
|
||||
export default class AppContainer extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
const httpLink = new HttpLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
|
||||
});
|
||||
import { ConfigProvider } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import moment from "moment";
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT_WS,
|
||||
options: {
|
||||
lazy: true,
|
||||
reconnect: true,
|
||||
connectionParams: async () => {
|
||||
//const token = localStorage.getItem("token");
|
||||
const token = await auth.currentUser.getIdToken(true);
|
||||
if (token) {
|
||||
return {
|
||||
headers: {
|
||||
authorization: token ? `Bearer ${token}` : ""
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
moment.locale("en-US");
|
||||
|
||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||
});
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT_WS,
|
||||
options: {
|
||||
lazy: true,
|
||||
reconnect: true,
|
||||
connectionParams: async () => {
|
||||
const token =
|
||||
auth.currentUser && (await auth.currentUser.getIdToken(true));
|
||||
if (token) {
|
||||
return {
|
||||
headers: {
|
||||
authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
const subscriptionMiddleware = {
|
||||
applyMiddleware: async (options, next) => {
|
||||
options.authToken = await auth.currentUser.getIdToken(true);
|
||||
next();
|
||||
}
|
||||
};
|
||||
wsLink.subscriptionClient.use([subscriptionMiddleware]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
// console.log(
|
||||
// "##Intercepted GQL Transaction : " +
|
||||
// definition.operation +
|
||||
// "|" +
|
||||
// definition.name.value +
|
||||
// "##",
|
||||
// query
|
||||
// );
|
||||
return (
|
||||
definition.kind === "OperationDefinition" &&
|
||||
definition.operation === "subscription"
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLink
|
||||
);
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
return auth.currentUser.getIdToken().then(token => {
|
||||
if (token) {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : ""
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return { headers };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryLink = new RetryLink({
|
||||
delay: {
|
||||
initial: 300,
|
||||
max: 5,
|
||||
jitter: true
|
||||
},
|
||||
attempts: {
|
||||
max: 5,
|
||||
retryIf: (error, _operation) => !!error
|
||||
}
|
||||
});
|
||||
|
||||
const middlewares = [];
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
middlewares.push(apolloLogger);
|
||||
}
|
||||
|
||||
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
|
||||
|
||||
const cache = new InMemoryCache();
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
connectToDevTools: true
|
||||
});
|
||||
|
||||
this.state = { client };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { client } = this.state;
|
||||
const subscriptionMiddleware = {
|
||||
applyMiddleware: async (options, next) => {
|
||||
options.authToken =
|
||||
auth.currentUser && (await auth.currentUser.getIdToken(true));
|
||||
next();
|
||||
},
|
||||
};
|
||||
wsLink.subscriptionClient.use([subscriptionMiddleware]);
|
||||
|
||||
const link = split(
|
||||
// split based on operation type
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
// console.log(
|
||||
// "##Intercepted GQL Transaction : " +
|
||||
// definition.operation +
|
||||
// "|" +
|
||||
// definition.name.value +
|
||||
// "##",
|
||||
// query
|
||||
// );
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
definition.kind === "OperationDefinition" &&
|
||||
definition.operation === "subscription"
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLink
|
||||
);
|
||||
|
||||
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 retryLink = new RetryLink({
|
||||
delay: {
|
||||
initial: 500,
|
||||
max: 5,
|
||||
jitter: true,
|
||||
},
|
||||
attempts: {
|
||||
max: 5,
|
||||
retryIf: (error, _operation) => !!error,
|
||||
},
|
||||
});
|
||||
|
||||
const middlewares = [];
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
middlewares.push(apolloLogger);
|
||||
}
|
||||
|
||||
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
|
||||
|
||||
const cache = new InMemoryCache({});
|
||||
|
||||
export const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
connectToDevTools: process.env.NODE_ENV !== "production",
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default function AppContainer() {
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
componentSize="small"
|
||||
input={{ autoComplete: "new-password" }}
|
||||
locale={enLocale}
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import "~antd/dist/antd.css";
|
||||
@@ -1,4 +1,5 @@
|
||||
import i18next from "i18next";
|
||||
import { Grid } from "antd";
|
||||
import "antd/dist/antd.css";
|
||||
import React, { lazy, Suspense, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -7,41 +8,42 @@ import { createStructuredSelector } from "reselect";
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||
//Component Imports
|
||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
||||
import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
|
||||
import PrivateRoute from "../utils/private-route";
|
||||
import "./App.css";
|
||||
import "./App.styles.scss";
|
||||
|
||||
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
|
||||
const ResetPassword = lazy(() =>
|
||||
import("../pages/reset-password/reset-password.component")
|
||||
);
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||
const Unauthorized = lazy(() =>
|
||||
import("../pages/unauthorized/unauthorized.component")
|
||||
);
|
||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||
const MobilePaymentContainer = lazy(() =>
|
||||
import("../pages/mobile-payment/mobile-payment.container")
|
||||
);
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkUserSession: () => dispatch(checkUserSession())
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkUserSession: () => dispatch(checkUserSession()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(({ checkUserSession, currentUser }) => {
|
||||
export function App({ checkUserSession, currentUser }) {
|
||||
useEffect(() => {
|
||||
checkUserSession();
|
||||
return () => {};
|
||||
}, [checkUserSession]);
|
||||
|
||||
const b = Grid.useBreakpoint();
|
||||
console.log("Breakpoints:", b);
|
||||
|
||||
const { t } = useTranslation();
|
||||
if (currentUser && currentUser.language)
|
||||
i18next.changeLanguage(currentUser.language, err => {
|
||||
if (err)
|
||||
return console.log("Error encountered when changing languages.", err);
|
||||
});
|
||||
|
||||
if (currentUser.authorized === null) {
|
||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||
@@ -51,20 +53,32 @@ export default connect(
|
||||
<div>
|
||||
<Switch>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
|
||||
<Route exact path='/' component={LandingPage} />
|
||||
<Route exact path='/unauthorized' component={Unauthorized} />
|
||||
|
||||
<Route exact path='/signin' component={SignInPage} />
|
||||
|
||||
<Suspense fallback={<LoadingSpinner message="App.Js Suspense" />}>
|
||||
<Route exact path="/" component={LandingPage} />
|
||||
<Route exact path="/unauthorized" component={Unauthorized} />
|
||||
<Route exact path="/signin" component={SignInPage} />
|
||||
<Route exact path="/resetpassword" component={ResetPassword} />
|
||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||
<Route
|
||||
exact
|
||||
path="/mp/:paymentIs"
|
||||
component={MobilePaymentContainer}
|
||||
/>
|
||||
<PrivateRoute
|
||||
isAuthorized={currentUser.authorized}
|
||||
path='/manage'
|
||||
path="/manage"
|
||||
component={ManagePage}
|
||||
/>
|
||||
<PrivateRoute
|
||||
isAuthorized={currentUser.authorized}
|
||||
path="/tech"
|
||||
component={TechPageContainer}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
|
||||
72
client/src/App/App.styles.scss
Normal file
72
client/src/App/App.styles.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
//Global Styles.
|
||||
|
||||
.imex-table-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
&__search {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.imex-flex-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&__grow {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__margin {
|
||||
margin: 0.2rem 0.2rem;
|
||||
}
|
||||
|
||||
&__margin-large {
|
||||
margin: 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
&__flex-space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipses {
|
||||
display: inline-block; /* for em, a, span, etc (inline by default) */
|
||||
text-overflow: ellipsis;
|
||||
width: calc(95%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tight-antd-rows {
|
||||
.ant-row {
|
||||
margin: 0rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.2rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.25rem;
|
||||
max-height: 0.25rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.2rem;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: #188fff;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
// background-color: red;
|
||||
padding: 0.2rem !important;
|
||||
}
|
||||
@@ -1,150 +1,96 @@
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
PaymentRequestButtonElement,
|
||||
useStripe
|
||||
} from "@stripe/react-stripe-js";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { EmailSettings } from "../../emails/constants";
|
||||
import {
|
||||
endLoading,
|
||||
startLoading,
|
||||
} from "../../redux/application/application.actions";
|
||||
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({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
load: () => dispatch(startLoading()),
|
||||
endload: () => dispatch(endLoading()),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(function Test({ setEmailOptions, load, endload, bodyshop }) {
|
||||
const [state, setState] = useState(temp);
|
||||
|
||||
const handleEditorChange = (content, editor) => {
|
||||
setState(content);
|
||||
};
|
||||
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={() => {
|
||||
axios
|
||||
.post("/render", {
|
||||
view: state,
|
||||
context: {
|
||||
people: ["Yehuda Katz", "Alan Johnson", "Charles Jolley"],
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
var newWin = window.open(
|
||||
"url",
|
||||
"windowName",
|
||||
"height=300,width=300"
|
||||
);
|
||||
newWin.document.write(r.data);
|
||||
});
|
||||
}}
|
||||
>
|
||||
TinyMCE
|
||||
</button>
|
||||
<Editor
|
||||
value={state}
|
||||
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
|
||||
init={{
|
||||
height: 500,
|
||||
//menubar: false,
|
||||
encoding: "raw",
|
||||
extended_valid_elements: "span",
|
||||
|
||||
plugins: [
|
||||
"advlist autolink lists link image charmap print preview anchor",
|
||||
"searchreplace visualblocks code fullscreen",
|
||||
"insertdatetime media table paste code help wordcount",
|
||||
],
|
||||
toolbar:
|
||||
"undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help",
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
setEmailOptions({
|
||||
messageOptions: {
|
||||
from: {
|
||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
||||
address: EmailSettings.fromAddress,
|
||||
},
|
||||
to: "patrickwf@gmail.com",
|
||||
to: ["patrickwf@gmail.com"],
|
||||
replyTo: bodyshop.email,
|
||||
Subject: "TODO FIX ME",
|
||||
},
|
||||
template: {
|
||||
name: "appointment_reminder",
|
||||
variables: { id: "2b42336f-b8de-4f04-a053-d6bff034d384" },
|
||||
name: TemplateList.parts_order_confirmation.key,
|
||||
variables: {
|
||||
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Set email config.
|
||||
send email
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEmailOptions({
|
||||
messageOptions: {
|
||||
from: {
|
||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
||||
address: EmailSettings.fromAddress,
|
||||
},
|
||||
to: "patrickwf@gmail.com",
|
||||
replyTo: bodyshop.email,
|
||||
Subject: "TODO FIX ME",
|
||||
},
|
||||
template: {
|
||||
name: "parts_order_confirmation",
|
||||
variables: { id: "6fea31e9-ea85-4c89-ac56-6f9cc84531fe" },
|
||||
},
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
||||
}}
|
||||
>
|
||||
Parts Order
|
||||
Log an ImEX Event.
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const temp = `<div style="font-family: Arial, Helvetica, sans-serif;">
|
||||
<p style="text-align: center;"><span>→<span> This is a full-featured editor demo. </span>Please explore! ←</span></p>
|
||||
<p style="text-align: center;"> </p>
|
||||
<h2 style="text-align: center;"><span>TinyMCE is the world's most customizable, and flexible, rich text editor.</span></h2>
|
||||
<p style="text-align: center;"><span><strong> A featherweight download, TinyMCE can handle any challenge you throw at it. </strong></span></p>
|
||||
<p style="text-align: center;"> </p>
|
||||
<p> </p>
|
||||
<table style="border-collapse: collapse; width: 85%; height: 86px; border-color: initial; border-style: solid; margin-left: auto; margin-right: auto;">
|
||||
<tbody>
|
||||
<tr style="height: 22px;">
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>🛠 50+ Plugins</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>💡 Premium Support</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>🖍 Custom Skins</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px;"><span>⚙ Full API Access</span></td>
|
||||
</tr>
|
||||
<tr style="height: 21px; display: none;">
|
||||
<td style="height: 21px; display: none;"><span>{{#each people}}</span></td>
|
||||
</tr>
|
||||
<tr style="height: 22px;">
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
|
||||
<td style="width: 25%; text-align: center; padding: 7px; height: 22px; border-style: solid; border-width: 1px;"><span>{{this}}</span></td>
|
||||
</tr>
|
||||
<tr style="height: 21px; display: none;">
|
||||
<td style="height: 21px;"><span>{{/each}}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Input, Table, Checkbox } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import InvoiceExportButton from "../invoice-export-button/invoice-export-button.component";
|
||||
import InvoiceExportAllButton from "../invoice-export-all-button/invoice-export-all-button.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function AccountingPayablesTableComponent({
|
||||
loading,
|
||||
invoices,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedInvoices, setSelectedInvoices] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("invoices.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/shop/vendors`,
|
||||
search: queryString.stringify({ selectedvendor: record.vendor.id }),
|
||||
}}
|
||||
>
|
||||
{record.vendor.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "invoice_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/invoices`,
|
||||
search: queryString.stringify({
|
||||
invoiceid: record.id,
|
||||
vendorid: record.vendor.id,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{record.invoice_number}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
|
||||
sorter: (a, b) => a.date - b.date,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.is_credit_memo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<InvoiceExportButton
|
||||
invoiceId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("accounting_payables_table_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? invoices.filter(
|
||||
(v) =>
|
||||
(v.vendor.name || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.invoice_number || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
)
|
||||
: invoices;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
<InvoiceExportAllButton
|
||||
invoiceIds={selectedInvoices}
|
||||
disabled={transInProgress || selectedInvoices.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedInvoices}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
size="small"
|
||||
pagination={{ position: "top", pageSize: 50 }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedInvoices(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedInvoices(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedInvoices,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function AccountingPayablesTableComponent({
|
||||
loading,
|
||||
payments,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.est_number"),
|
||||
dataIndex: "est_number",
|
||||
key: "est_number",
|
||||
sorter: (a, b) => a.job.est_number - b.job.est_number,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.est_number}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr_ln" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
{`${record.job.ownr_fn || ""} ${record.job.ownr_ln || ""}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.job.ownr_fn || ""} ${
|
||||
record.job.ownr_ln || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.memo"),
|
||||
dataIndex: "memo",
|
||||
key: "memo",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.transactionid"),
|
||||
dataIndex: "transactionid",
|
||||
key: "transactionid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.stripeid"),
|
||||
dataIndex: "stripeid",
|
||||
key: "stripeid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.exportedat"),
|
||||
dataIndex: "exportedat",
|
||||
key: "exportedat",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<PaymentExportButton
|
||||
paymentId={record.id}
|
||||
disabled={transInProgress || !!record.exportedat}
|
||||
loadingCallback={setTransInProgress}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("account_payments_table_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? payments.filter(
|
||||
(v) =>
|
||||
(v.vendor.name || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.invoice_number || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
)
|
||||
: payments;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
<PaymentsExportAllButton
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
size='small'
|
||||
pagination={{ position: "top", pageSize: 50 }}
|
||||
columns={columns}
|
||||
rowKey='id'
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedPayments,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Input, Table, Button } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { JobsExportAllButton } from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.est_number"),
|
||||
dataIndex: "est_number",
|
||||
key: "est_number",
|
||||
sorter: (a, b) => a.est_number - b.est_number,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>{record.est_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => a.status - b.status,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.owner ? (
|
||||
<Link to={"/manage/owners/" + record.owner.id}>
|
||||
{`${record.ownr_fn || ""} ${record.ownr_ln || ""}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? (
|
||||
<span>{record.clm_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.clm_total ? (
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<div style={{ display: "flex" }}>
|
||||
<JobExportButton
|
||||
jobId={record.id}
|
||||
disabled={!!record.date_exported}
|
||||
/>
|
||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("accounting_receivables_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? jobs.filter(
|
||||
(v) =>
|
||||
(v.ro_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.est_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_co_nm || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: jobs;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div className="imex-table-header">
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
/>
|
||||
<Input.Search
|
||||
className="imex-table-header__search"
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedJobs,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Alert } from "antd";
|
||||
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import React from "react";
|
||||
|
||||
export default function AlertComponent(props) {
|
||||
if (props.type === "error") logImEXEvent("alert_render", { ...props });
|
||||
return <Alert {...props} />;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { MdRemoveCircleOutline } from "react-icons/md";
|
||||
|
||||
export default function AllocationsLabelComponent({ allocation, handleClick }) {
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>
|
||||
{`${allocation.employee.first_name || ""} ${allocation.employee
|
||||
.last_name || ""} (${allocation.hours || ""})`}
|
||||
{`${allocation.employee.first_name || ""} ${
|
||||
allocation.employee.last_name || ""
|
||||
} (${allocation.hours || ""})`}
|
||||
</span>
|
||||
<Icon
|
||||
style={{ color: "red", padding: "0px 4px" }}
|
||||
|
||||
@@ -8,19 +8,21 @@ import { useTranslation } from "react-i18next";
|
||||
export default function AllocationsLabelContainer({ allocation, refetch }) {
|
||||
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
|
||||
const { t } = useTranslation();
|
||||
const handleClick = e => {
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
deleteAllocation({ variables: { id: allocation.id } })
|
||||
.then(r => {
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.deleted")
|
||||
message: t("allocations.successes.deleted"),
|
||||
});
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
notification["error"]({ message: t("allocations.errors.deleting") });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AllocationsLabelComponent
|
||||
allocation={allocation}
|
||||
|
||||
@@ -3,16 +3,19 @@ import AuditTrailListComponent from "./audit-trail-list.component";
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function AuditTrailListContainer({ recordId }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||
variables: { id: recordId },
|
||||
fetchPolicy: "network-only"
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
logImEXEvent("audittrail_view", { recordId });
|
||||
return (
|
||||
<div>
|
||||
{error ? (
|
||||
<AlertComponent type="error" message={error.message} />
|
||||
<AlertComponent type='error' message={error.message} />
|
||||
) : (
|
||||
<AuditTrailListComponent
|
||||
loading={loading}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Tag, Popover } from "antd";
|
||||
import React from "react";
|
||||
import Barcode from "react-barcode";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export default function BarcodePopupComponent({ value }) {
|
||||
export default function BarcodePopupComponent({ value, children }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -10,12 +10,11 @@ export default function BarcodePopupComponent({ value }) {
|
||||
content={
|
||||
<Barcode
|
||||
value={value}
|
||||
background="transparent"
|
||||
background='transparent'
|
||||
displayValue={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Tag>{t("general.labels.barcode")}</Tag>
|
||||
}>
|
||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { HomeFilled } from "@ant-design/icons";
|
||||
import { Breadcrumb } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||
import { Link } from "react-router-dom";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
breadcrumbs: selectBreadcrumbs,
|
||||
@@ -11,18 +13,24 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function BreadCrumbs({ breadcrumbs }) {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>Home</Breadcrumb.Item>
|
||||
{breadcrumbs.map((item) =>
|
||||
item.link ? (
|
||||
<Breadcrumb.Item key={item.label}>
|
||||
<Link to={item.link}>{item.label} </Link>
|
||||
</Breadcrumb.Item>
|
||||
) : (
|
||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
||||
)
|
||||
)}
|
||||
</Breadcrumb>
|
||||
<div className="breadcrumb-container imex-flex-row">
|
||||
<Breadcrumb separator=">">
|
||||
<Breadcrumb.Item>
|
||||
<Link to={`/manage`}>
|
||||
<HomeFilled />
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
{breadcrumbs.map((item) =>
|
||||
item.link ? (
|
||||
<Breadcrumb.Item key={item.label}>
|
||||
<Link to={item.link}>{item.label} </Link>
|
||||
</Breadcrumb.Item>
|
||||
) : (
|
||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
||||
)
|
||||
)}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.breadcrumb-container {
|
||||
margin: 0.5rem 4rem;
|
||||
}
|
||||
43
client/src/components/chat-affix/chat-affix.component.jsx
Normal file
43
client/src/components/chat-affix/chat-affix.component.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { MessageOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatPopupComponent from '../chat-popup/chat-popup.component'
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
chatVisible: selectChatVisible,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
});
|
||||
|
||||
export function ChatAffixComponent({
|
||||
chatVisible,
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
unreadCount,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
<Card size='small'>
|
||||
{chatVisible ? (
|
||||
<ChatPopupComponent conversationList={conversationList} />
|
||||
) : (
|
||||
<div
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ cursor: "pointer" }}>
|
||||
<MessageOutlined />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatAffixComponent);
|
||||
32
client/src/components/chat-affix/chat-affix.container.jsx
Normal file
32
client/src/components/chat-affix/chat-affix.container.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useSubscription } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatAffixComponent from "./chat-affix.component";
|
||||
import { Affix } from "antd";
|
||||
import "./chat-affix.styles.scss";
|
||||
export default function ChatAffixContainer() {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION
|
||||
);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
return (
|
||||
<Affix className='chat-affix'>
|
||||
<div>
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
4
client/src/components/chat-affix/chat-affix.styles.scss
Normal file
4
client/src/components/chat-affix/chat-affix.styles.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.chat-affix {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
}
|
||||
@@ -1,41 +1,67 @@
|
||||
import { ShrinkOutlined } from "@ant-design/icons";
|
||||
import { Badge } from "antd";
|
||||
import { Badge, List } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
openConversation,
|
||||
toggleChatVisible
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
openConversation: number => dispatch(openConversation(number))
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedConversation: (conversationId) =>
|
||||
dispatch(setSelectedConversation(conversationId)),
|
||||
});
|
||||
|
||||
export function ChatConversationListComponent({
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
openConversation
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='chat-overlay-open'>
|
||||
<ShrinkOutlined onClick={() => toggleChatVisible()} />
|
||||
{conversationList.map(item => (
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0}>
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ cursor: "pointer", display: "block" }}
|
||||
onClick={() =>
|
||||
openConversation({ phone_num: item.phone_num, id: item.id })
|
||||
}>
|
||||
<div>
|
||||
<PhoneNumberFormatter>{item.phone_num}</PhoneNumberFormatter>
|
||||
</div>
|
||||
</div>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<List
|
||||
bordered
|
||||
size="small"
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<PhoneFormatter>{item.phone_num}</PhoneFormatter>}
|
||||
description={
|
||||
item.job_conversations.length > 0 ? (
|
||||
<div>
|
||||
{item.job_conversations.map(
|
||||
(j) =>
|
||||
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
t("messaging.labels.nojobs")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ChatConversationListComponent);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatConversationListComponent);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.chat-list-selected-conversation {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #ff7a00;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Tag } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||
|
||||
const handleRemoveTag = (jobId) => {
|
||||
const convId = jobConversations[0].conversationid;
|
||||
if (!!convId) {
|
||||
removeJobConversation({
|
||||
variables: {
|
||||
conversationId: convId,
|
||||
jobId: jobId,
|
||||
},
|
||||
});
|
||||
logImEXEvent("messaging_remove_job_tag", {
|
||||
conversationId: convId,
|
||||
jobId: jobId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{jobConversations.map((item) => (
|
||||
<Tag
|
||||
key={item.job.id}
|
||||
closable
|
||||
color='blue'
|
||||
style={{ cursor: "pointer" }}
|
||||
onClose={() => handleRemoveTag(item.job.id)}>
|
||||
<Link to={`/manage/jobs/${item.job.id}`}>
|
||||
{item.job.ro_number || "?"}
|
||||
</Link>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Space } from "antd";
|
||||
import React from "react";
|
||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<div>
|
||||
<Space>
|
||||
<strong>{conversation && conversation.phone_num}</strong>
|
||||
<span>
|
||||
{conversation.job_conversations.map(
|
||||
(j) =>
|
||||
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} | `
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
<div className="imex-flex-row imex-flex-row__margin">
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
<ChatPresetsComponent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { CloseCircleFilled } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
closeConversation,
|
||||
sendMessage,
|
||||
toggleConversationVisible
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleConversationVisible: conversationId =>
|
||||
dispatch(toggleConversationVisible(conversationId)),
|
||||
closeConversation: phone => dispatch(closeConversation(phone)),
|
||||
sendMessage: message => dispatch(sendMessage(message))
|
||||
});
|
||||
|
||||
function ChatConversationClosedComponent({
|
||||
conversation,
|
||||
toggleConversationVisible,
|
||||
closeConversation
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className='chat-conversation-closed'
|
||||
onClick={() => toggleConversationVisible(conversation.id)}>
|
||||
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
|
||||
<CloseCircleFilled
|
||||
onClick={() => closeConversation(conversation.phone_num)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(ChatConversationClosedComponent);
|
||||
@@ -1,29 +1,32 @@
|
||||
import { Badge, Card } from "antd";
|
||||
import React from "react";
|
||||
import ChatConversationClosedComponent from "./chat-conversation.closed.component";
|
||||
import ChatConversationOpenComponent from "./chat-conversation.open.component";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatConversationTitle from "../chat-conversation-title/chat-conversation-title.component";
|
||||
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
|
||||
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||
import "./chat-conversation.styles.scss";
|
||||
|
||||
export default function ChatConversationComponent({
|
||||
conversation,
|
||||
messages,
|
||||
subState,
|
||||
unreadCount
|
||||
conversation,
|
||||
handleMarkConversationAsRead,
|
||||
}) {
|
||||
const [loading, error] = subState;
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const messages = (conversation && conversation.messages) || [];
|
||||
|
||||
return (
|
||||
<div className='chat-conversation'>
|
||||
<Badge count={unreadCount}>
|
||||
<Card size='small'>
|
||||
{conversation.open ? (
|
||||
<ChatConversationOpenComponent
|
||||
messages={messages}
|
||||
conversation={conversation}
|
||||
subState={subState}
|
||||
/>
|
||||
) : (
|
||||
<ChatConversationClosedComponent conversation={conversation} />
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
<div
|
||||
className="chat-conversation"
|
||||
onMouseDown={handleMarkConversationAsRead}
|
||||
onKeyDown={handleMarkConversationAsRead}
|
||||
>
|
||||
<ChatConversationTitle conversation={conversation} />
|
||||
<ChatMessageListComponent messages={messages} />
|
||||
<ChatSendMessage conversation={conversation} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
import { useSubscription } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { useMutation, useSubscription } from "@apollo/react-hooks";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
});
|
||||
|
||||
export default function ChatConversationContainer({ conversation }) {
|
||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
||||
|
||||
export function ChatConversationContainer({ selectedConversation }) {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
||||
{
|
||||
variables: { conversationId: conversation.id }
|
||||
variables: { conversationId: selectedConversation },
|
||||
}
|
||||
);
|
||||
|
||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||
|
||||
const [markConversationRead] = useMutation(
|
||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||
{
|
||||
variables: { conversationId: selectedConversation },
|
||||
}
|
||||
);
|
||||
|
||||
const unreadCount =
|
||||
(data &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk.messages_aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
|
||||
0;
|
||||
|
||||
const handleMarkConversationAsRead = async () => {
|
||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||
setMarkingAsReadInProgress(true);
|
||||
await markConversationRead();
|
||||
setMarkingAsReadInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatConversationComponent
|
||||
subState={[loading, error]}
|
||||
conversation={conversation}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk.messages_aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate &&
|
||||
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
|
||||
0
|
||||
}
|
||||
messages={
|
||||
(data &&
|
||||
data.conversations_by_pk &&
|
||||
data.conversations_by_pk.messages) ||
|
||||
[]
|
||||
}
|
||||
conversation={data ? data.conversations_by_pk : {}}
|
||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { toggleConversationVisible } from "../../redux/messaging/messaging.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
|
||||
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { ShrinkOutlined } from "@ant-design/icons";
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleConversationVisible: conversation =>
|
||||
dispatch(toggleConversationVisible(conversation))
|
||||
});
|
||||
|
||||
export function ChatConversationOpenComponent({
|
||||
conversation,
|
||||
messages,
|
||||
subState,
|
||||
toggleConversationVisible
|
||||
}) {
|
||||
const [loading, error] = subState;
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
return (
|
||||
<div className='chat-conversation-open'>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleConversationVisible(conversation.id)}
|
||||
/>
|
||||
<ChatMessageListComponent messages={messages} />
|
||||
<ChatSendMessage conversation={conversation} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ChatConversationOpenComponent);
|
||||
@@ -0,0 +1,6 @@
|
||||
.chat-conversation {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin: 0rem 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Affix } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectConversations } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatMessagesButtonContainer from "../chat-messages-button/chat-messages-button.container";
|
||||
import "./chat-dock.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
activeConversations: selectConversations
|
||||
});
|
||||
|
||||
export function ChatOverlayContainer({ activeConversations }) {
|
||||
return (
|
||||
<Affix offsetBottom={0}>
|
||||
<div className='chat-dock'>
|
||||
<ChatMessagesButtonContainer />
|
||||
{activeConversations
|
||||
? activeConversations.map(conversation => (
|
||||
<ChatConversationContainer
|
||||
conversation={conversation}
|
||||
key={conversation.id}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ChatOverlayContainer);
|
||||
@@ -1,188 +0,0 @@
|
||||
.chat-dock {
|
||||
z-index: 5;
|
||||
//overflow-x: scroll;
|
||||
// overflow-y: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.chat-conversation {
|
||||
margin: 2em 1em 0em 1em;
|
||||
}
|
||||
|
||||
.chat-conversation-open {
|
||||
height: 500px;
|
||||
}
|
||||
// .chat-messages {
|
||||
// height: 80%;
|
||||
// overflow-x: hidden;
|
||||
// overflow-y: scroll;
|
||||
// flex-grow: 1;
|
||||
|
||||
// ul {
|
||||
// list-style: none;
|
||||
// margin: 0;
|
||||
// padding: 0;
|
||||
// }
|
||||
|
||||
// ul li {
|
||||
// display: inline-block;
|
||||
// clear: both;
|
||||
// padding: 3px 10px;
|
||||
// border-radius: 30px;
|
||||
// margin-bottom: 2px;
|
||||
// }
|
||||
|
||||
// .inbound {
|
||||
// background: #eee;
|
||||
// float: left;
|
||||
// }
|
||||
|
||||
// .outbound {
|
||||
// float: right;
|
||||
// background: #0084ff;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// .inbound + .outbound {
|
||||
// border-bottom-right-radius: 5px;
|
||||
// }
|
||||
|
||||
// .outbound + .outbound {
|
||||
// border-top-right-radius: 5px;
|
||||
// border-bottom-right-radius: 5px;
|
||||
// }
|
||||
|
||||
// .outbound:last-of-type {
|
||||
// border-bottom-right-radius: 30px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.messages {
|
||||
height: auto;
|
||||
min-height: calc(100% - 10px);
|
||||
max-height: calc(100% - 93px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@media screen and (max-width: 735px) {
|
||||
.messages {
|
||||
max-height: calc(100% - 105px);
|
||||
}
|
||||
}
|
||||
.messages::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
.messages::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.messages ul li {
|
||||
display: inline-block;
|
||||
clear: both;
|
||||
//float: left;
|
||||
margin: 5px;
|
||||
width: calc(100% - 25px);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.messages ul li:nth-last-child(1) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.messages ul li.sent img {
|
||||
margin: 6px 8px 0 0;
|
||||
}
|
||||
.messages ul li.sent p {
|
||||
background: #435f7a;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.messages ul li.replies img {
|
||||
float: right;
|
||||
margin: 6px 0 0 8px;
|
||||
}
|
||||
.messages ul li.replies p {
|
||||
background: #f5f5f5;
|
||||
float: right;
|
||||
}
|
||||
.messages ul li img {
|
||||
width: 22px;
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
}
|
||||
.messages ul li p {
|
||||
display: inline-block;
|
||||
padding: 10px 15px;
|
||||
border-radius: 20px;
|
||||
max-width: 205px;
|
||||
line-height: 130%;
|
||||
}
|
||||
@media screen and (min-width: 735px) {
|
||||
.messages ul li p {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
.message-input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
}
|
||||
.message-input .wrap {
|
||||
position: relative;
|
||||
}
|
||||
.message-input .wrap input {
|
||||
font-family: "proxima-nova", "Source Sans Pro", sans-serif;
|
||||
float: left;
|
||||
border: none;
|
||||
width: calc(100% - 90px);
|
||||
padding: 11px 32px 10px 8px;
|
||||
font-size: 0.8em;
|
||||
color: #32465a;
|
||||
}
|
||||
@media screen and (max-width: 735px) {
|
||||
.message-input .wrap input {
|
||||
padding: 15px 32px 16px 8px;
|
||||
}
|
||||
}
|
||||
.message-input .wrap input:focus {
|
||||
outline: none;
|
||||
}
|
||||
.message-input .wrap .attachment {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
z-index: 4;
|
||||
margin-top: 10px;
|
||||
font-size: 1.1em;
|
||||
color: #435f7a;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media screen and (max-width: 735px) {
|
||||
.message-input .wrap .attachment {
|
||||
margin-top: 17px;
|
||||
right: 65px;
|
||||
}
|
||||
}
|
||||
.message-input .wrap .attachment:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.message-input .wrap button {
|
||||
float: right;
|
||||
border: none;
|
||||
width: 50px;
|
||||
padding: 12px 0;
|
||||
cursor: pointer;
|
||||
background: #32465a;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
@media screen and (max-width: 735px) {
|
||||
.message-input .wrap button {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
.message-input .wrap button:hover {
|
||||
background: #435f7a;
|
||||
}
|
||||
.message-input .wrap button:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { MessageFilled } from "@ant-design/icons";
|
||||
import { Badge, Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
chatVisible: selectChatVisible
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||
});
|
||||
|
||||
export function ChatWindowComponent({
|
||||
chatVisible,
|
||||
toggleChatVisible,
|
||||
conversationList,
|
||||
unreadCount
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='chat-conversation'>
|
||||
<Badge count={unreadCount}>
|
||||
<Card size='small'>
|
||||
{chatVisible ? (
|
||||
<ChatConversationListComponent
|
||||
conversationList={conversationList}
|
||||
/>
|
||||
) : (
|
||||
<div onClick={() => toggleChatVisible()}>
|
||||
<MessageFilled />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatWindowComponent);
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useSubscription } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatMessagesButtonComponent from "./chat-messages-button.component";
|
||||
|
||||
export default function ChatMessagesButtonContainer() {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION
|
||||
);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
return (
|
||||
<ChatMessagesButtonComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +1,98 @@
|
||||
import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons";
|
||||
import Icon from "@ant-design/icons";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { FaCheck, FaCheckDouble } from "react-icons/fa";
|
||||
import {
|
||||
AutoSizer,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
List,
|
||||
} from "react-virtualized";
|
||||
import "./chat-message-list.styles.scss";
|
||||
|
||||
export default function ChatMessageListComponent({ messages }) {
|
||||
const messagesEndRef = useRef(null);
|
||||
const virtualizedListRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
console.log("use");
|
||||
!!messagesEndRef.current &&
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
const _cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
// minHeight: 50,
|
||||
defaultHeight: 100,
|
||||
});
|
||||
|
||||
const scrollToBottom = (renderedrows) => {
|
||||
//console.log("Scrolling to", messages.length);
|
||||
// !!virtualizedListRef.current &&
|
||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
||||
//Scrolling does not work on this version of React.
|
||||
};
|
||||
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
const StatusRender = status => {
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <CheckOutlined style={{ margin: "2px", float: "right" }} />;
|
||||
case "delivered":
|
||||
return (
|
||||
<CheckCircleOutlined style={{ margin: "2px", float: "right" }} />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
||||
return (
|
||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div
|
||||
ref={registerChild}
|
||||
onLoad={measure}
|
||||
style={style}
|
||||
className={`${
|
||||
messages[index].isoutbound ? "mine messages" : "yours messages"
|
||||
}`}>
|
||||
<div className='message msgmargin'>
|
||||
{MessageRender(messages[index])}
|
||||
{StatusRender(messages[index].status)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='messages'>
|
||||
<ul>
|
||||
{messages.map(item => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`${item.isoutbound ? "replies" : "sent"}`}>
|
||||
<p>
|
||||
{item.text}
|
||||
{StatusRender(item.status)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
<li ref={messagesEndRef} />
|
||||
</ul>
|
||||
<div className='chat'>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={virtualizedListRef}
|
||||
width={width}
|
||||
height={height}
|
||||
rowHeight={_cache.rowHeight}
|
||||
rowRenderer={_rowRenderer}
|
||||
rowCount={messages.length}
|
||||
overscanRowCount={10}
|
||||
estimatedRowSize={150}
|
||||
scrollToIndex={messages.length}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageRender = (message) => {
|
||||
if (message.image) {
|
||||
return (
|
||||
<a href={message.image_path} target='__blank'>
|
||||
<img
|
||||
alt='Received'
|
||||
className='message-img'
|
||||
src={message.image_path}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return <span>{message.text}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const StatusRender = (status) => {
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <Icon component={FaCheck} className='message-icon' />;
|
||||
case "delivered":
|
||||
return <Icon component={FaCheckDouble} className='message-icon' />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
.message-icon {
|
||||
//position: absolute;
|
||||
// bottom: 0rem;
|
||||
color: whitesmoke;
|
||||
border: #000000;
|
||||
margin-left: 0.2rem;
|
||||
margin-right: 0rem;
|
||||
// z-index: 5;
|
||||
}
|
||||
|
||||
.chat {
|
||||
flex: 1;
|
||||
//width: 300px;
|
||||
//border: solid 1px #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.8rem 0rem;
|
||||
}
|
||||
|
||||
.messages {
|
||||
//margin-top: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message {
|
||||
border-radius: 20px;
|
||||
padding: 0.25rem 0.8rem;
|
||||
//margin-top: 5px;
|
||||
// margin-bottom: 5px;
|
||||
//display: inline-block;
|
||||
|
||||
.message-img {
|
||||
max-width: 3rem;
|
||||
max-height: 3rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.yours {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.msgmargin {
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.yours .message {
|
||||
margin-right: 20%;
|
||||
background-color: #eee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.yours .message.last:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: 0;
|
||||
left: -7px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: #eee;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
.yours .message.last:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
left: -10px;
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.mine {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.mine .message {
|
||||
color: white;
|
||||
margin-left: 25%;
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mine .message.last:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: 0;
|
||||
right: -8px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
border-bottom-left-radius: 15px;
|
||||
}
|
||||
|
||||
.mine .message.last:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
right: -10px;
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
import { MessageFilled } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openConversation } from "../../redux/messaging/messaging.actions";
|
||||
import { MessageFilled } from "@ant-design/icons";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
openConversation: phone => dispatch(openConversation(phone))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(function ChatOpenButton({ openConversation, phone }) {
|
||||
export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
||||
return (
|
||||
<MessageFilled
|
||||
style={{ margin: 4 }}
|
||||
onClick={() => openConversation(phone)}
|
||||
onClick={() => openChatByPhone({ phone_num: phone, jobid: jobid })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(ChatOpenButton);
|
||||
|
||||
47
client/src/components/chat-popup/chat-popup.component.jsx
Normal file
47
client/src/components/chat-popup/chat-popup.component.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ShrinkOutlined } from "@ant-design/icons";
|
||||
import { Col, Row, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import "./chat-popup.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
});
|
||||
|
||||
export function ChatPopupComponent({
|
||||
conversationList,
|
||||
selectedConversation,
|
||||
toggleChatVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="chat-popup">
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
<ChatConversationListComponent conversationList={conversationList} />
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
{selectedConversation ? <ChatConversationContainer /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
|
||||
20
client/src/components/chat-popup/chat-popup.styles.scss
Normal file
20
client/src/components/chat-popup/chat-popup.styles.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.chat-popup {
|
||||
width: 90vw;
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-popup-content {
|
||||
//height: 50vh;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.chat-popup {
|
||||
width: 60vw;
|
||||
height: 55vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
});
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => setMessage(i.text)} onItemHover key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("messaging.labels.presets")} <DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatPresetsComponent);
|
||||
@@ -1,60 +1,80 @@
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { sendMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
sendMessage,
|
||||
setMessage,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import {
|
||||
selectIsSending,
|
||||
selectMessage,
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
sendMessage: message => dispatch(sendMessage(message))
|
||||
bodyshop: selectBodyshop,
|
||||
isSending: selectIsSending,
|
||||
message: selectMessage,
|
||||
});
|
||||
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
|
||||
const [message, setMessage] = useState("");
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
sendMessage: (message) => dispatch(sendMessage(message)),
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
});
|
||||
|
||||
function ChatSendMessageComponent({
|
||||
conversation,
|
||||
bodyshop,
|
||||
sendMessage,
|
||||
isSending,
|
||||
message,
|
||||
setMessage,
|
||||
}) {
|
||||
const inputArea = useRef(null);
|
||||
useEffect(() => {
|
||||
if (conversation.isSending === false) {
|
||||
setMessage("");
|
||||
}
|
||||
}, [conversation, setMessage]);
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
logImEXEvent("messaging_send_message");
|
||||
sendMessage({
|
||||
to: conversation.phone_num,
|
||||
body: message,
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id
|
||||
conversationid: conversation.id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex " }}>
|
||||
<div className="imex-flex-row">
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
autoFocus
|
||||
suffix={<span>a</span>}
|
||||
ref={inputArea}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={message}
|
||||
disabled={conversation.isSending}
|
||||
disabled={isSending}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
onPressEnter={event => {
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||
<Spin
|
||||
style={{ display: `${conversation.isSending ? "" : "none"}` }}
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
indicator={
|
||||
<LoadingOutlined
|
||||
style={{
|
||||
fontSize: 24
|
||||
fontSize: 24,
|
||||
}}
|
||||
spin
|
||||
/>
|
||||
|
||||
50
client/src/components/chat-tag-ro/chat-tag-ro.component.jsx
Normal file
50
client/src/components/chat-tag-ro/chat-tag-ro.component.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { AutoComplete } from "antd";
|
||||
import { LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChatTagRoComponent({
|
||||
searchQueryState,
|
||||
roOptions,
|
||||
loading,
|
||||
executeSearch,
|
||||
handleInsertTag,
|
||||
setVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setSearchQuery = searchQueryState[1];
|
||||
const handleSearchQuery = (value) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
executeSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<AutoComplete
|
||||
style={{ width: 100 }}
|
||||
onSearch={handleSearchQuery}
|
||||
onSelect={handleInsertTag}
|
||||
placeholder={t("general.labels.search")}
|
||||
onKeyDown={handleKeyDown}>
|
||||
{roOptions.map((item, idx) => (
|
||||
<AutoComplete.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||
item.ownr_ln || ""
|
||||
}`}
|
||||
</AutoComplete.Option>
|
||||
))}
|
||||
</AutoComplete>
|
||||
{loading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
67
client/src/components/chat-tag-ro/chat-tag-ro.container.jsx
Normal file
67
client/src/components/chat-tag-ro/chat-tag-ro.container.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from "react";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function ChatTagRoContainer({ conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const searchQueryState = useState("");
|
||||
const searchText = searchQueryState[0];
|
||||
|
||||
const [loadRo, { called, loading, data, refetch }] = useLazyQuery(
|
||||
SEARCH_FOR_JOBS,
|
||||
{
|
||||
variables: { search: `%${searchText}%` },
|
||||
}
|
||||
);
|
||||
|
||||
const executeSearch = () => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: searchText });
|
||||
if (called) refetch();
|
||||
else {
|
||||
loadRo();
|
||||
}
|
||||
};
|
||||
|
||||
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
|
||||
variables: { conversationId: conversation.id },
|
||||
});
|
||||
|
||||
const handleInsertTag = (value, option) => {
|
||||
logImEXEvent("messaging_add_job_tag");
|
||||
insertTag({ variables: { jobId: option.key } });
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const existingJobTags = conversation.job_conversations.map((i) => i.jobid);
|
||||
|
||||
const roOptions = data
|
||||
? data.jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{visible ? (
|
||||
<ChatTagRo
|
||||
loading={loading}
|
||||
searchQueryState={searchQueryState}
|
||||
roOptions={roOptions}
|
||||
executeSearch={executeSearch}
|
||||
handleInsertTag={handleInsertTag}
|
||||
setVisible={setVisible}
|
||||
/>
|
||||
) : (
|
||||
<Tag onClick={() => setVisible(true)}>
|
||||
<PlusOutlined />
|
||||
{t("messaging.actions.link")}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { Form, Checkbox } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
valuePropName="checked"
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Checkbox disabled={readOnly} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import FormTypes from "./config-form-types";
|
||||
|
||||
export default function ConfirmFormComponents({ componentList, readOnly }) {
|
||||
return (
|
||||
<div>
|
||||
{componentList.map((f, idx) => {
|
||||
const Comp = FormTypes[f.type];
|
||||
|
||||
if (!!Comp) {
|
||||
return <Comp key={idx} formItem={f} readOnly={readOnly} />;
|
||||
} else {
|
||||
return <div key={idx}>Error</div>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import CheckboxFormItem from "./checkbox/checkbox.component";
|
||||
import Rate from "./rate/rate.component";
|
||||
import Slider from "./slider/slider.component";
|
||||
import Text from "./text/text.component";
|
||||
import Textarea from "./textarea/textarea.component";
|
||||
|
||||
export default {
|
||||
checkbox: CheckboxFormItem,
|
||||
slider: Slider,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
rate: Rate,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Form, Rate } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Rate disabled={readOnly} allowHalf />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Form, Slider } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required, min, max } = formItem;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider disabled={readOnly} min={min || 0} max={max || 10} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Form, Input } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={readOnly} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Form, Input } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required, rows } = formItem;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea disabled={readOnly} rows={rows || 4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
27
client/src/components/conflict/conflict.component.jsx
Normal file
27
client/src/components/conflict/conflict.component.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { Result, Button } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ConflictComponent() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.instanceconflictitle")}
|
||||
extra={
|
||||
<div>
|
||||
<div>{t("general.labels.instanceconflictext")}</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,12 @@ export default function ContractsCarsComponent({
|
||||
loading,
|
||||
data,
|
||||
selectedCar,
|
||||
handleSelect
|
||||
handleSelect,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
search: ""
|
||||
search: "",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -24,7 +24,7 @@ export default function ContractsCarsComponent({
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
@@ -32,21 +32,24 @@ export default function ContractsCarsComponent({
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => <div>{t(record.status)}</div>,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "year" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "make" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
@@ -54,7 +57,7 @@ export default function ContractsCarsComponent({
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
@@ -62,8 +65,8 @@ export default function ContractsCarsComponent({
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
|
||||
}
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
@@ -74,7 +77,7 @@ export default function ContractsCarsComponent({
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
cc =>
|
||||
(cc) =>
|
||||
(cc.fleetnumber || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
@@ -100,19 +103,19 @@ export default function ContractsCarsComponent({
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={e => setState({ ...state, search: e.target.value })}
|
||||
onChange={(e) => setState({ ...state, search: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
columns={columns.map((item) => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCar]
|
||||
selectedRowKeys: [selectedCar],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, notification, Popover, Radio, Form, InputNumber } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "react-apollo";
|
||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||
import moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { useHistory } from "react-router-dom";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
||||
const history = useHistory();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
|
||||
const contractLength = moment(contract.actualreturn).diff(
|
||||
moment(contract.start),
|
||||
"days"
|
||||
);
|
||||
const billingLines = [
|
||||
{
|
||||
unq_seq: 1,
|
||||
line_no: 1,
|
||||
line_ref: 1,
|
||||
line_desc: t("contracts.fields.dailyrate"),
|
||||
db_price: contract.dailyrate,
|
||||
act_price: contract.dailyrate,
|
||||
part_qty: contractLength,
|
||||
part_type: "CCDR",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
// mod_lbr_ty: "PAL",
|
||||
},
|
||||
];
|
||||
|
||||
const mileageDiff =
|
||||
contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
|
||||
if (mileageDiff > 0) {
|
||||
billingLines.push({
|
||||
unq_seq: 2,
|
||||
line_no: 2,
|
||||
line_ref: 2,
|
||||
line_desc: "Fuel Surcharge",
|
||||
db_price: contract.excesskmrate,
|
||||
act_price: contract.excesskmrate,
|
||||
part_type: "CCM",
|
||||
part_qty: mileageDiff,
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (values.refuelqty > 0) {
|
||||
billingLines.push({
|
||||
unq_seq: 3,
|
||||
line_no: 3,
|
||||
line_ref: 3,
|
||||
line_desc: t("contracts.fields.refuelcharge"),
|
||||
db_price: contract.refuelcharge,
|
||||
act_price: contract.refuelcharge,
|
||||
part_qty: values.refuelqty,
|
||||
part_type: "CCF",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
if (values.applyCleanupCharge) {
|
||||
billingLines.push({
|
||||
unq_seq: 4,
|
||||
line_no: 4,
|
||||
line_ref: 4,
|
||||
line_desc: t("contracts.fields.cleanupcharge"),
|
||||
db_price: contract.cleanupcharge,
|
||||
act_price: contract.cleanupcharge,
|
||||
part_qty: 1,
|
||||
part_type: "CCC",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
if (contract.damagewaiver) {
|
||||
//Add for cleanup fee.
|
||||
billingLines.push({
|
||||
unq_seq: 5,
|
||||
line_no: 5,
|
||||
line_ref: 5,
|
||||
line_desc: t("contracts.fields.damagewaiver"),
|
||||
db_price: contract.damagewaiver,
|
||||
act_price: contract.damagewaiver,
|
||||
part_type: "CCD",
|
||||
part_qty: 1,
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const newJob = {
|
||||
// converted: true,
|
||||
shopid: bodyshop.id,
|
||||
ownerid: contract.job.ownerid,
|
||||
vehicleid: contract.job.vehicleid,
|
||||
federal_tax_rate: bodyshop.invoice_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.invoice_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.invoice_tax_rates.local_tax_rate / 100,
|
||||
clm_no: `${contract.job.clm_no}-CC`,
|
||||
clm_total: 1234, //TODO
|
||||
ownr_fn: contract.job.owner.ownr_fn,
|
||||
ownr_ln: contract.job.owner.ownr_ln,
|
||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||
ownr_ph1: contract.job.owner.ownr_ph1,
|
||||
ownr_ea: contract.job.owner.ownr_ea,
|
||||
v_model_desc: contract.job.vehicle.v_model_desc,
|
||||
v_model_yr: contract.job.vehicle.v_model_yr,
|
||||
v_make_desc: contract.job.vehicle.v_make_desc,
|
||||
v_vin: contract.job.vehicle.v_vin,
|
||||
status: bodyshop.md_ro_statuses.default_completed,
|
||||
notes: {
|
||||
data: [
|
||||
{
|
||||
text: t("contracts.labels.noteconvertedfrom", {
|
||||
agreementnumber: contract.agreementnumber,
|
||||
}),
|
||||
created_by: currentUser.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
joblines: {
|
||||
data: billingLines,
|
||||
},
|
||||
parts_tax_rates: {
|
||||
CCDR: {
|
||||
prt_type: "CCDR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
},
|
||||
CCF: {
|
||||
prt_type: "CCF",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
},
|
||||
CCM: {
|
||||
prt_type: "CCM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
},
|
||||
CCC: {
|
||||
prt_type: "CCC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
},
|
||||
CCD: {
|
||||
prt_type: "CCD",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = await insertJob({
|
||||
variables: { job: [newJob] },
|
||||
// refetchQueries: ["GET_JOB_BY_PK"],
|
||||
// awaitRefetchQueries: true,
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.inserting", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.created"),
|
||||
onClick: () => {
|
||||
history.push(
|
||||
`/manage/jobs/${result.data.insert_jobs.returning[0].id}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.applycleanupcharge")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={"applyCleanupCharge"}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>{t("general.labels.yes")}</Radio>
|
||||
<Radio value={false}>{t("general.labels.no")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.refuelqty")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={"refuelqty"}
|
||||
>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button onClick={() => setVisible(true)} loading={loading}>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ContractConvertToRo);
|
||||
@@ -1,264 +1,308 @@
|
||||
import React, { useState } from "react";
|
||||
import { Form, Input, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input, DatePicker, InputNumber, Button } from "antd";
|
||||
import aamva from "aamva";
|
||||
import InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||
|
||||
export default function ContractFormComponent() {
|
||||
const [state, setState] = useState("");
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
export default function ContractFormComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div style={{ background: "#f00" }}>
|
||||
TEST AREA
|
||||
<Input value={state} onChange={e => setState(e.target.value)} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log("state", state);
|
||||
//let data = state;
|
||||
|
||||
var data =
|
||||
"%FLDELRAY BEACH^DOE$JOHN$^4818 S FEDERAL BLVD^ ? ;6360100462172082009=2101198299090=? #! 33435 I 1600 ECCECC00000?";
|
||||
data = data.replace(/\n/, "");
|
||||
// replace spaces with regular space
|
||||
data = data.replace(/\s/g, " ");
|
||||
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
|
||||
console.log("data", data);
|
||||
console.log("track", track);
|
||||
const a = aamva.stripe(data);
|
||||
console.log(JSON.stringify(a));
|
||||
}}
|
||||
>
|
||||
Decode
|
||||
</Button>
|
||||
<div className="imex-flex-row__grow imex-flex-row__margin-large">
|
||||
<FormFieldsChanged form={form} />
|
||||
</div>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.scheduledreturn")}
|
||||
name="scheduledreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.scheduledreturn")}
|
||||
name="scheduledreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmstart")}
|
||||
name="kmstart"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
name="driver_dlnumber"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlexpiry")}
|
||||
name="driver_dlexpiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlst")}
|
||||
name="driver_dlst"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_fn")}
|
||||
name="driver_fn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ln")}
|
||||
name="driver_ln"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr1")}
|
||||
name="driver_addr1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_city")}
|
||||
name="driver_city"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_state")}
|
||||
name="driver_state"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_zip")}
|
||||
name="driver_zip"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ph1")}
|
||||
name="driver_ph1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputPhone />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dob")}
|
||||
name="driver_dob"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_num")}
|
||||
name="cc_num"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_expiry")}
|
||||
name="cc_expiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_cardholder")}
|
||||
name="cc_cardholder"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmstart")}
|
||||
name="kmstart"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
name="driver_dlnumber"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlexpiry")}
|
||||
name="driver_dlexpiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlst")}
|
||||
name="driver_dlst"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_fn")}
|
||||
name="driver_fn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ln")}
|
||||
name="driver_ln"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr1")}
|
||||
name="driver_addr1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr2")}
|
||||
name="driver_addr2"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_city")}
|
||||
name="driver_city"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_state")}
|
||||
name="driver_state"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_zip")}
|
||||
name="driver_zip"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ph1")}
|
||||
name="driver_ph1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputPhone />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dob")}
|
||||
name="driver_dob"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_num")}
|
||||
name="cc_num"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_expiry")}
|
||||
name="cc_expiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cc_cardholder")}
|
||||
name="cc_cardholder"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actax")} name="actax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.dailyfreekm")} name="dailyfreekm">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.refuelcharge")}
|
||||
name="refuelcharge"
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.excesskmrate")}
|
||||
name="excesskmrate"
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cleanupcharge")}
|
||||
name="cleanupcharge"
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.damagewaiver")}
|
||||
name="damagewaiver"
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Button, Input, Modal, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import aamva from "../../utils/aamva";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function ContractLicenseDecodeButton({ form }) {
|
||||
const { t } = useTranslation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [decodedBarcode, setDecodedBarcode] = useState(null);
|
||||
console.log("form", form);
|
||||
|
||||
const handleDecode = (e) => {
|
||||
logImEXEvent("contract_license_decode");
|
||||
setLoading(true);
|
||||
const aamvaParse = aamva.parse(e.currentTarget.value);
|
||||
console.log("AAMVA", aamvaParse);
|
||||
setDecodedBarcode(aamvaParse);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleInsertForm = () => {
|
||||
logImEXEvent("contract_license_decode_fill_form");
|
||||
|
||||
const values = {
|
||||
driver_dlnumber: decodedBarcode.dl,
|
||||
driver_dlexpiry: moment(
|
||||
`20${decodedBarcode.expiration_date}${moment(
|
||||
decodedBarcode.birthday
|
||||
).format("DD")}`
|
||||
),
|
||||
driver_dlst: decodedBarcode.state,
|
||||
driver_fn: decodedBarcode.name.first,
|
||||
driver_ln: decodedBarcode.name.last,
|
||||
driver_addr1: decodedBarcode.address,
|
||||
driver_city: decodedBarcode.city,
|
||||
driver_state: decodedBarcode.state,
|
||||
driver_zip: decodedBarcode.postal_code,
|
||||
driver_dob: moment(decodedBarcode.birthday),
|
||||
};
|
||||
|
||||
form.setFieldsValue(values);
|
||||
setModalVisible(false);
|
||||
setDecodedBarcode(null);
|
||||
};
|
||||
const handleClick = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
okText={t("contracts.actions.senddltoform")}
|
||||
onOk={handleInsertForm}
|
||||
okButtonProps={{ disabled: !!!decodedBarcode }}
|
||||
onCancel={handleCancel}>
|
||||
<div>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
allowClear
|
||||
onChange={(e) => {
|
||||
if (!loading) setLoading(true);
|
||||
}}
|
||||
onPressEnter={handleDecode}
|
||||
/>
|
||||
</div>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
{decodedBarcode ? (
|
||||
<div>
|
||||
<DataLabel label={t("contracts.fields.driver_dlst")}>
|
||||
{decodedBarcode.state}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlnumber")}>
|
||||
{decodedBarcode.dl}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_fn")}>
|
||||
{decodedBarcode.name.first}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_ln")}>
|
||||
{decodedBarcode.name.last}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr1")}>
|
||||
{decodedBarcode.address}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr2")}>
|
||||
{decodedBarcode.address}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlexpiry")}>
|
||||
{moment(
|
||||
`20${decodedBarcode.expiration_date}${moment(
|
||||
decodedBarcode.birthday
|
||||
).format("DD")}`
|
||||
).format("MM/DD/YYYY")}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dob")}>
|
||||
{moment(decodedBarcode.birthday).format("MM/DD/YYYY")}
|
||||
</DataLabel>
|
||||
<div>
|
||||
<Typography.Title level={4}>
|
||||
{t("contracts.labels.correctdataonform")}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("contracts.labels.waitingforscan")}</div>
|
||||
)}
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button onClick={handleClick}>
|
||||
{t("contracts.actions.decodelicense")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, forwardRef } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
|
||||
const ContractStatusComponent = ({
|
||||
value = "contracts.status.new",
|
||||
onChange
|
||||
}) => {
|
||||
const ContractStatusComponent = (
|
||||
{ value = "contracts.status.new", onChange },
|
||||
ref
|
||||
) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [option, onChange]);
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
width: 100,
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
@@ -32,4 +32,4 @@ const ContractStatusComponent = ({
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default ContractStatusComponent;
|
||||
export default forwardRef(ContractStatusComponent);
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Table } from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component";
|
||||
|
||||
export default function ContractsList({ loading, contracts }) {
|
||||
export default function ContractsList({ loading, contracts, refetch, total }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
const history = useHistory();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page } = search;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -26,21 +32,17 @@ export default function ContractsList({ loading, contracts }) {
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
@@ -50,7 +52,17 @@ export default function ContractsList({ loading, contracts }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
//sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
//sortOrder:
|
||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
@@ -59,7 +71,7 @@ export default function ContractsList({ loading, contracts }) {
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
render: (text, record) => t(record.status),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
@@ -68,7 +80,9 @@ export default function ContractsList({ loading, contracts }) {
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.start}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
@@ -79,21 +93,61 @@ export default function ContractsList({ loading, contracts }) {
|
||||
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
)
|
||||
}
|
||||
<DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.actualreturn"),
|
||||
dataIndex: "actualreturn",
|
||||
key: "actualreturn",
|
||||
sorter: (a, b) => alphaSort(a.actualreturn, b.actualreturn),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actualreturn" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => (
|
||||
<div className="imex-table-header">
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<div>
|
||||
<TimeTicketsDatesSelector />
|
||||
</div>
|
||||
<div className="imex-table-header__search">
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
onSearch={(value) => {
|
||||
search.search = value;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
scroll={{ x: "50%", y: "40rem" }}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { Button, Form, Input, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { Form, Input, InputNumber, DatePicker, Button } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.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";
|
||||
|
||||
export default function CourtesyCarCreateFormComponent() {
|
||||
export default function CourtesyCarCreateFormComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<div className="imex-flex-row__grow imex-flex-row__margin-large">
|
||||
<FormFieldsChanged form={form} />
|
||||
</div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -30,8 +35,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -42,8 +47,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -54,8 +59,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -66,8 +71,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -78,8 +83,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
@@ -94,7 +99,7 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
label={t("courtesycars.fields.purchasedate")}
|
||||
name="purchasedate"
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.servicestartdate")}
|
||||
@@ -106,13 +111,13 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
label={t("courtesycars.fields.serviceenddate")}
|
||||
name="serviceenddate"
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.leaseenddate")}
|
||||
name="leaseenddate"
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
@@ -120,8 +125,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
@@ -132,8 +137,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
@@ -144,11 +149,11 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input />
|
||||
@@ -162,8 +167,8 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
@@ -174,11 +179,11 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
@@ -186,11 +191,11 @@ export default function CourtesyCarCreateFormComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Slider } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
|
||||
const CourtesyCarFuelComponent = ({ value = 100, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [option, onChange]);
|
||||
}, [value, option, onChange]);
|
||||
|
||||
const marks = {
|
||||
0: {
|
||||
style: {
|
||||
color: "#f50"
|
||||
color: "#f50",
|
||||
},
|
||||
label: t("courtesycars.labels.fuel.empty")
|
||||
label: t("courtesycars.labels.fuel.empty"),
|
||||
},
|
||||
13: t("courtesycars.labels.fuel.18"),
|
||||
25: t("courtesycars.labels.fuel.14"),
|
||||
@@ -28,14 +28,15 @@ const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
|
||||
88: t("courtesycars.labels.fuel.78"),
|
||||
100: {
|
||||
style: {
|
||||
color: "#008000"
|
||||
color: "#008000",
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
|
||||
}
|
||||
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Slider
|
||||
ref={ref}
|
||||
marks={marks}
|
||||
defaultValue={value}
|
||||
onChange={setOption}
|
||||
@@ -43,4 +44,4 @@ const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CourtesyCarFuelComponent;
|
||||
export default forwardRef(CourtesyCarFuelComponent);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Form, DatePicker, InputNumber } from "antd";
|
||||
import { Form, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
import FormDatePicker from '../form-date-picker/form-date-picker.component';
|
||||
|
||||
export default function CourtesyCarReturnModalComponent() {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,7 +19,7 @@ export default function CourtesyCarReturnModalComponent() {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<DatePicker />
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmend")}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user