Merged in dev-patrick (pull request #7)

Merge Dev Changes
This commit is contained in:
Snapt Software
2020-08-04 23:41:05 +00:00
1174 changed files with 86778 additions and 9855 deletions

13
.gitignore vendored
View File

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

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
server.env
AWSAccessKeyId=AKIAJYNXY5KCA25PB2JA
AWSSecretKey=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
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

File diff suppressed because one or more lines are too long

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

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

44
admin/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

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

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
admin/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

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

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

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

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

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

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

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

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

View 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");
}
});

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
@import "~antd/dist/antd.css";

View File

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

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

View File

@@ -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>&rarr;<span> This is a full-featured editor demo. </span>Please explore! &larr;</span></p>
<p style="text-align: center;">&nbsp;</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;">&nbsp;</p>
<p>&nbsp;</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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
.breadcrumb-container {
margin: 0.5rem 4rem;
}

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

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

View File

@@ -0,0 +1,4 @@
.chat-affix {
position: absolute;
bottom: 2vh;
}

View File

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

View File

@@ -0,0 +1,10 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-item {
&:hover {
cursor: pointer;
color: #ff7a00;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
.chat-conversation {
display: flex;
height: 100%;
margin: 0rem 0.5rem;
flex-direction: column;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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