@@ -1,53 +0,0 @@
|
||||
####################################################################################################
|
||||
#### Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
####
|
||||
#### Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
|
||||
#### except in compliance with the License. A copy of the License is located at
|
||||
####
|
||||
#### http://aws.amazon.com/apache2.0/
|
||||
####
|
||||
#### or in the "license" file accompanying this file. This file is distributed on an "AS IS"
|
||||
#### BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
#### License for the specific language governing permissions and limitations under the License.
|
||||
####################################################################################################
|
||||
|
||||
####################################################################################################
|
||||
#### This configuration file adds a listener to the Application Load Balancer for port 443, this new listener
|
||||
#### requires the ARN of a public website certificate create residing in the certificate manager service.
|
||||
#### The configuration file also modifies the default port 80 listener attached to an Application Load Balancer
|
||||
#### to automatically redirect incoming connections on HTTP to HTTPS.
|
||||
#### This will not work with an environment using the load balancer type Classic or Network.
|
||||
#### Do not use this configuration file if a listener has already been created for port 443 from the console.
|
||||
####################################################################################################
|
||||
|
||||
Resources:
|
||||
AWSEBV2LoadBalancerListener:
|
||||
Type: 'AWS::ElasticLoadBalancingV2::Listener'
|
||||
Properties:
|
||||
DefaultActions:
|
||||
- Type: redirect
|
||||
RedirectConfig:
|
||||
Protocol: HTTPS
|
||||
Port: '443'
|
||||
Host: '#{host}'
|
||||
Path: '/#{path}'
|
||||
Query: '#{query}'
|
||||
StatusCode: HTTP_301
|
||||
LoadBalancerArn:
|
||||
Ref: AWSEBV2LoadBalancer
|
||||
Port: 80
|
||||
Protocol: HTTP
|
||||
AWSEBV2LoadBalancerListenerHTTPS:
|
||||
Type: 'AWS::ElasticLoadBalancingV2::Listener'
|
||||
Properties:
|
||||
Certificates:
|
||||
- CertificateArn: arn:aws:acm:ca-central-1:714144183158:certificate/c6a0fcde-b959-4aee-afc6-934e27c4962b
|
||||
DefaultActions:
|
||||
- Type: forward
|
||||
TargetGroupArn:
|
||||
Ref: AWSEBV2LoadBalancerTargetGroup
|
||||
LoadBalancerArn:
|
||||
Ref: AWSEBV2LoadBalancer
|
||||
Port: 443
|
||||
Protocol: HTTPS
|
||||
|
||||
12
.eslintrc.json
Normal file
12
.eslintrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": false,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,8 @@ client.pnp.js
|
||||
admin/node_modules
|
||||
admin/.pnp
|
||||
admin.pnp.js
|
||||
jsreport/node_modules
|
||||
jsreport/auth-server/node_modules
|
||||
# testing
|
||||
/coverage
|
||||
client/coverage
|
||||
@@ -37,6 +39,7 @@ client/yarn-error.log*
|
||||
admin/npm-debug.log*
|
||||
admin/yarn-debug.log*
|
||||
admin/yarn-error.log*
|
||||
client/.eslintcache
|
||||
|
||||
#Firebase Ignore
|
||||
# Logs
|
||||
|
||||
@@ -12,15 +12,15 @@ To Start Hasura CLI:
|
||||
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!
|
||||
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
|
||||
NGROK TEsting:
|
||||
|
||||
./ngrok.exe http https://localhost:5000 -host-header="localhost:5000"
|
||||
./ngrok.exe http http://localhost:5000 -host-header="localhost:5000"
|
||||
|
||||
Finding deadfiles - run from client directory
|
||||
npx deadfile ./src/index.js --exclude build templates
|
||||
|
||||
cd client && yarn build && cd build && scp -r ** imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd ..
|
||||
cd client && yarn build && cd build && scp -r \*\* imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd ..
|
||||
|
||||
gq https://bodyshop-dev-db.herokuapp.com/v1/graphql -H "X-Hasura-Admin-Secret: Dev-BodyShopAppBySnaptSoftware\!" --introspect > schema.graphql
|
||||
|
||||
40
_reference/JSReportSetup.md
Normal file
40
_reference/JSReportSetup.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# install node.js
|
||||
wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
|
||||
# you may need to reopen terminal
|
||||
nvm install 8.11.3
|
||||
|
||||
mkdir jsreportapp
|
||||
cd jsreportapp
|
||||
npm i -g jsreport-cli
|
||||
jsreport init
|
||||
jsreport configure
|
||||
|
||||
# chrome dependencies
|
||||
sudo apt-get install -y libgconf-2-4
|
||||
sudo wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst --no-install-recommends
|
||||
|
||||
# on ubuntu 20 run also
|
||||
sudo apt-get install -y libxtst6 libxss1
|
||||
|
||||
# start jsreport to see it running on port 5488
|
||||
jsreport start
|
||||
|
||||
# the next steps are optional to start jsreport on boot
|
||||
npm install pm2 -g
|
||||
pm2 start server.js
|
||||
pm2 startup
|
||||
# run the output of previous command
|
||||
|
||||
# optionally if you want to use older phantomjs for pdf rendering
|
||||
sudo apt-get install -y --no-install-recommends gnupg git curl wget ca-certificates
|
||||
sudo apt-get install -y --no-install-recommends xfonts-base xfonts-75dpi
|
||||
npm i jsreport-phantom-pdf --save --save-exact
|
||||
|
||||
Running on port 80 and 443 without SU
|
||||
$ setcap 'cap_net_bind_service=+ep' /path/to/.nvm/v0.10.17/bin/node
|
||||
$ apt-get remove nginx
|
||||
$ cd /path/to/app
|
||||
$ PORT=80 node app
|
||||
18103
admin/package-lock.json
generated
Normal file
18103
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,14 +13,14 @@
|
||||
"apollo-link-logger": "^1.2.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"firebase": "^7.21.0",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql": "^15.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-hasura-graphql": "^0.1.12",
|
||||
"react": "^16.13.1",
|
||||
"react": "^17.0.1",
|
||||
"react-admin": "^3.8.5",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-scripts": "3.4.3"
|
||||
"react-scripts": "4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "set PORT=3001 && react-scripts start",
|
||||
|
||||
@@ -5,7 +5,7 @@ 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" />
|
||||
|
||||
@@ -141,7 +141,6 @@ const JobsEdit = (props) => (
|
||||
<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" />
|
||||
</FormTab>
|
||||
<FormTab label="Insurance info">
|
||||
@@ -283,7 +282,6 @@ const JobsEdit = (props) => (
|
||||
<FormTab label="Other">
|
||||
<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" />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import React from "react";
|
||||
import {
|
||||
Datagrid,
|
||||
Filter,
|
||||
List,
|
||||
ReferenceField,
|
||||
TextField,
|
||||
SelectInput,
|
||||
TextInput,
|
||||
TextField,
|
||||
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}>
|
||||
@@ -20,7 +20,7 @@ const JobsList = (props) => (
|
||||
<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" />
|
||||
|
||||
@@ -128,7 +128,6 @@ const JobsShow = (props) => (
|
||||
<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" />
|
||||
@@ -221,7 +220,7 @@ const JobsShow = (props) => (
|
||||
<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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import gql from "graphql-tag";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const QUERY_ALL_SHOPS = gql`
|
||||
query QUERY_ALL_SHOPS {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
client/debug.log
Normal file
1
client/debug.log
Normal file
@@ -0,0 +1 @@
|
||||
[1207/095430.554:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
|
||||
12758
client/licenses.txt
Normal file
12758
client/licenses.txt
Normal file
File diff suppressed because it is too large
Load Diff
45978
client/package-lock.json
generated
Normal file
45978
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,52 @@
|
||||
{
|
||||
"name": "bodyshop",
|
||||
"version": "0.1.0001",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"@lourenci/react-kanban": "^2.0.0",
|
||||
"@stripe/react-stripe-js": "^1.1.2",
|
||||
"@stripe/stripe-js": "^1.9.0",
|
||||
"@tanem/react-nprogress": "^3.0.46",
|
||||
"@tinymce/tinymce-react": "^3.7.0",
|
||||
"antd": "^4.6.6",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"@apollo/client": "^3.3.11",
|
||||
"@fingerprintjs/fingerprintjs": "^3.0.6",
|
||||
"@lourenci/react-kanban": "^2.1.0",
|
||||
"@sentry/react": "^6.2.0",
|
||||
"@sentry/tracing": "^6.2.0",
|
||||
"@stripe/react-stripe-js": "^1.2.2",
|
||||
"@stripe/stripe-js": "^1.12.1",
|
||||
"@tanem/react-nprogress": "^3.0.56",
|
||||
"@tinymce/tinymce-react": "^3.10.2",
|
||||
"antd": "^4.12.3",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"axios": "^0.20.0",
|
||||
"codemirror": "^5.58.1",
|
||||
"codemirror-graphql": "^0.12.2",
|
||||
"axios": "^0.21.1",
|
||||
"dinero.js": "^1.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fingerprintjs2": "^2.1.2",
|
||||
"firebase": "^7.22.1",
|
||||
"graphql": "^15.3.0",
|
||||
"i18next": "^19.8.2",
|
||||
"firebase": "^8.2.9",
|
||||
"graphql": "^15.5.0",
|
||||
"i18next": "^19.8.9",
|
||||
"i18next-browser-languagedetector": "^6.0.1",
|
||||
"inline-css": "^2.6.3",
|
||||
"jsoneditor": "^9.1.1",
|
||||
"jsoneditor-react": "^3.0.1",
|
||||
"jsoneditor": "^9.1.10",
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.9.11",
|
||||
"logrocket": "^1.0.13",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"phone": "^2.4.16",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.13.5",
|
||||
"react": "^16.13.1",
|
||||
"react-apollo": "^3.1.5",
|
||||
"react-big-calendar": "^0.28.0",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-color": "^2.18.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-drag-listview": "^0.1.7",
|
||||
"react-email-editor": "^1.1.1",
|
||||
"react-ga": "^3.1.2",
|
||||
"query-string": "^6.14.0",
|
||||
"react": "^17.0.1",
|
||||
"react-big-calendar": "^0.30.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-drag-listview": "^0.1.8",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-i18next": "^11.7.3",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-moment": "^1.0.0",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-i18next": "^11.8.7",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-number-format": "^4.4.4",
|
||||
"react-phone-input-2": "^2.13.9",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-resizable": "^1.11.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.3",
|
||||
"react-trello": "^2.2.8",
|
||||
"react-virtualized": "^9.22.2",
|
||||
"recharts": "^1.8.5",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.0.7",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
@@ -64,6 +59,7 @@
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "react-scripts start",
|
||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
||||
"build-deploy": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build && s3cmd sync build/* s3://imex-online-production && echo '🚀 Deployed!'",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
||||
@@ -84,10 +80,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/react-testing": "^4.0.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.0"
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
client/public/file.png
Normal file
BIN
client/public/file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -5,10 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#002366" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,58 +0,0 @@
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
line-height: 1.25;
|
||||
padding: 10mm 10mm 10mm 10mm !important;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
table.imex-table {
|
||||
border: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
font-size: inherit;
|
||||
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
table.imex-table caption {
|
||||
/* font-size: 1.5em; */
|
||||
margin: 0.5em 0 0.75em;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
table.imex-table tr {
|
||||
/* background-color: #f8f8f8; */
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.2rem;
|
||||
font-size: inherit;
|
||||
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
table.imex-table th,
|
||||
table.imex-table td {
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
font-size: inherit;
|
||||
}
|
||||
table.imex-table th.left,
|
||||
table.imex-table td.left {
|
||||
padding: 0.3rem;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
table.imex-table th {
|
||||
/* font-size: 0.85em; */
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
/* display: table-header-group; */
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApolloProvider } from "@apollo/react-common";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import { ConfigProvider } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import LogRocket from "logrocket";
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 AboutPage from "../pages/about/about.page";
|
||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
||||
import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||
@@ -62,6 +63,9 @@ export function App({ checkUserSession, currentUser }) {
|
||||
<ErrorBoundary>
|
||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Route exact path="/about" component={AboutPage} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Route
|
||||
exact
|
||||
|
||||
@@ -49,25 +49,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
::-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-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 {
|
||||
// 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;
|
||||
}
|
||||
// ::-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;
|
||||
}
|
||||
|
||||
.ant-input-number-input,
|
||||
.ant-input-number,
|
||||
.ant-picker-input,
|
||||
.ant-picker,
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.production-alert {
|
||||
animation: alertBlinker 1s linear infinite;
|
||||
color: blue;
|
||||
}
|
||||
@keyframes alertBlinker {
|
||||
50% {
|
||||
color: red;
|
||||
opacity: 100;
|
||||
//opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.production-completion-1 {
|
||||
animation: production-completion-1-blinker 5s linear infinite;
|
||||
}
|
||||
@keyframes production-completion-1-blinker {
|
||||
50% {
|
||||
background: rgba(207, 12, 12, 0.555);
|
||||
}
|
||||
}
|
||||
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
right: -5px;
|
||||
cursor: col-resize;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.production-list-min-height {
|
||||
min-height: 19px;
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { AlertOutlined } from "@ant-design/icons";
|
||||
import { Button, notification } from "antd";
|
||||
import i18n from "i18next";
|
||||
import React from "react";
|
||||
import * as serviceWorker from "../serviceWorker";
|
||||
|
||||
const onServiceWorkerUpdate = (registration) => {
|
||||
console.log("[RSW] onServiceWorkerUpdate", registration);
|
||||
|
||||
const key = `open${Date.now()}`;
|
||||
const btn = (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
if (registration && registration.waiting) {
|
||||
await registration.unregister();
|
||||
// Makes Workbox call skipWaiting()
|
||||
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
||||
// Once the service worker is unregistered, we can reload the page to let
|
||||
// the browser download a fresh copy of our app (invalidating the cache)
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.t("general.actions.refresh")}
|
||||
</Button>
|
||||
);
|
||||
notification.open({
|
||||
icon: <AlertOutlined />,
|
||||
message: i18n.t("general.messages.newversiontitle"),
|
||||
description: i18n.t("general.messages.newversionmessage"),
|
||||
duration: 0,
|
||||
btn,
|
||||
key,
|
||||
});
|
||||
};
|
||||
|
||||
// if (process.env.NODE_ENV === "production") {
|
||||
// console.log("SWR Registering SW...");
|
||||
console.log("Registering Service Worker...");
|
||||
serviceWorker.register({ onUpdate: onServiceWorkerUpdate });
|
||||
// }
|
||||
BIN
client/src/assets/file.png
Normal file
BIN
client/src/assets/file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -72,7 +72,7 @@ function Test({ bodyshop, setEmailOptions }) {
|
||||
replyTo: bodyshop.email,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList().parts_order_confirmation.key,
|
||||
name: TemplateList().parts_order.key,
|
||||
variables: {
|
||||
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
||||
},
|
||||
|
||||
@@ -150,19 +150,20 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<div className="imex-table-header">
|
||||
<PayableExportAll
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
/>
|
||||
<Input
|
||||
className="imex-table-header__search"
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
<PayableExportAll
|
||||
billIds={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { DateFormatter, 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,
|
||||
@@ -38,17 +38,21 @@ export default function AccountingPayablesTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.est_number"),
|
||||
dataIndex: "est_number",
|
||||
key: "est_number",
|
||||
sorter: (a, b) => a.job.est_number - b.job.est_number,
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => alphaSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.est_number}
|
||||
</Link>
|
||||
),
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => alphaSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Input, Table, Button } from "antd";
|
||||
import { Button, Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
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 }) {
|
||||
@@ -34,17 +34,7 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
<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",
|
||||
@@ -156,10 +146,6 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.est_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Alert } from "antd";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import React from "react";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function AlertComponent(props) {
|
||||
if (props.type === "error") logImEXEvent("alert_render", { ...props });
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Alert from "./alert.component";
|
||||
import { MockedProvider } from "@apollo/react-testing";
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
describe("Alert component", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message."
|
||||
message: "Test error message.",
|
||||
};
|
||||
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||
import { Select } from "antd";
|
||||
const div = document.createElement("div");
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
|
||||
describe("AllocationsAssignmentComponent component", () => {
|
||||
let wrapper;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { notification } from "antd";
|
||||
@@ -8,29 +8,29 @@ import { notification } from "antd";
|
||||
export default function AllocationsAssignmentContainer({
|
||||
jobLineId,
|
||||
hours,
|
||||
refetch
|
||||
refetch,
|
||||
}) {
|
||||
const visibilityState = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
joblineid: jobLineId,
|
||||
hours: parseFloat(hours),
|
||||
employeeid: null
|
||||
employeeid: null,
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
|
||||
const handleAssignment = () => {
|
||||
insertAllocation({ variables: { alloc: { ...assignment } } })
|
||||
.then(r => {
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.save")
|
||||
message: t("allocations.successes.save"),
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("employees.errors.saving", { message: error.message })
|
||||
message: t("employees.errors.saving", { message: error.message }),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useState } from "react";
|
||||
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { notification } from "antd";
|
||||
|
||||
export default function AllocationsBulkAssignmentContainer({
|
||||
jobLines,
|
||||
refetch
|
||||
refetch,
|
||||
}) {
|
||||
const visibilityState = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
employeeid: null
|
||||
employeeid: null,
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function AllocationsBulkAssignmentContainer({
|
||||
acc.push({
|
||||
joblineid: value.id,
|
||||
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||
employeeid: assignment.employeeid
|
||||
employeeid: assignment.employeeid,
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
insertAllocation({ variables: { alloc: allocs } }).then(r => {
|
||||
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save")
|
||||
message: t("employees.successes.save"),
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
@@ -37,7 +37,7 @@ export default function AllocationsBulkAssignmentContainer({
|
||||
|
||||
return (
|
||||
<AllocationsBulkAssignment
|
||||
disabled={jobLines.length > 0 ? false : true}
|
||||
disabled={jobLines.length > 0 ? false : true}
|
||||
handleAssignment={handleAssignment}
|
||||
assignment={assignment}
|
||||
setAssignment={setAssignment}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
||||
import { notification } from "antd";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
@@ -15,7 +15,7 @@ export default function AuditTrailListContainer({ recordId }) {
|
||||
return (
|
||||
<div>
|
||||
{error ? (
|
||||
<AlertComponent type='error' message={error.message} />
|
||||
<AlertComponent type="error" message={error.message} />
|
||||
) : (
|
||||
<AuditTrailListComponent
|
||||
loading={loading}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
export default function BillDeleteButton({ bill }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
const result = await deleteBill({
|
||||
variables: { billId: bill.id },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
bills(existingBills, { readField }) {
|
||||
return existingBills.filter(
|
||||
(billref) => bill.id !== readField("id", billref)
|
||||
);
|
||||
},
|
||||
search_bills(existingBills, { readField }) {
|
||||
return existingBills.filter(
|
||||
(billref) => bill.id !== readField("id", billref)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<RbacWrapper action="bills:delete" noauth={<></>}>
|
||||
<Button disabled={bill.exported} onClick={handleDelete} loading={loading}>
|
||||
{t("general.actions.delete")}
|
||||
</Button>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Form } from "antd";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Form, Modal, notification } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import {
|
||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
UPDATE_JOB,
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
billEnterModal: selectBillEnterModal,
|
||||
@@ -34,84 +39,154 @@ function BillEnterModalContainer({
|
||||
const { t } = useTranslation();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleFinish = (values) => {
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const { upload, location, ...remainingValues } = values;
|
||||
insertBill({
|
||||
|
||||
let adjustmentsToInsert = {};
|
||||
|
||||
const r1 = await insertBill({
|
||||
variables: {
|
||||
bill: [
|
||||
Object.assign({}, remainingValues, {
|
||||
{
|
||||
...remainingValues,
|
||||
billlines: {
|
||||
data:
|
||||
remainingValues.billlines &&
|
||||
remainingValues.billlines.map((i) => {
|
||||
const {
|
||||
deductfromlabor,
|
||||
lbr_adjustment,
|
||||
location: lineLocation,
|
||||
...restI
|
||||
} = i;
|
||||
|
||||
if (deductfromlabor) {
|
||||
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
|
||||
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
|
||||
restI.actual_price / lbr_adjustment.rate;
|
||||
}
|
||||
return {
|
||||
...i,
|
||||
...restI,
|
||||
deductedfromlbr: deductfromlabor,
|
||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const billId = r.data.insert_bills.returning[0].id;
|
||||
});
|
||||
console.log("adjustmentsToInsert", adjustmentsToInsert);
|
||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||
if (adjKeys.length > 0) {
|
||||
//Query the adjustments, merge, and update them.
|
||||
const existingAdjustments = await client.query({
|
||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
variables: {
|
||||
id: values.jobid,
|
||||
},
|
||||
});
|
||||
|
||||
updateJobLines({
|
||||
variables: {
|
||||
ids: remainingValues.billlines
|
||||
.filter((il) => il.joblineid !== "noline")
|
||||
.map((li) => li.joblineid),
|
||||
status: bodyshop.md_order_statuses.default_received || "Received*",
|
||||
location: location,
|
||||
},
|
||||
}).then((joblineresult) => {
|
||||
/////////////////////////
|
||||
if (upload && upload.length > 0) {
|
||||
//insert Each of the documents?
|
||||
upload.forEach((u) => {
|
||||
handleUpload(
|
||||
{ file: u.originFileObj },
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
///////////////////////////
|
||||
setLoading(false);
|
||||
notification["success"]({
|
||||
message: t("bills.successes.created"),
|
||||
});
|
||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||
const newAdjustments = _.cloneDeep(
|
||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ billlines: [] });
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
setEnterAgain(false);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
adjKeys.forEach((key) => {
|
||||
newAdjustments[key] =
|
||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB,
|
||||
variables: {
|
||||
jobId: values.jobid,
|
||||
job: { lbr_adjustments: newAdjustments },
|
||||
},
|
||||
});
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.creating", {
|
||||
message: JSON.stringify(error),
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!r1.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.creating", {
|
||||
message: JSON.stringify(r1.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const billId = r1.data.insert_bills.returning[0].id;
|
||||
|
||||
await Promise.all(
|
||||
remainingValues.billlines
|
||||
.filter((il) => il.joblineid !== "noline")
|
||||
.map((li) => {
|
||||
return updateJobLines({
|
||||
variables: {
|
||||
lineId: li.joblineid,
|
||||
line: {
|
||||
location: li.location || location,
|
||||
status:
|
||||
bodyshop.md_order_statuses.default_received || "Received*",
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// await updateJobLines({
|
||||
// variables: {
|
||||
// ids: remainingValues.billlines
|
||||
// .filter((il) => il.joblineid !== "noline")
|
||||
// .map((li) => li.joblineid),
|
||||
// status: bodyshop.md_order_statuses.default_received || "Received*",
|
||||
// location: location,
|
||||
// },
|
||||
// });
|
||||
|
||||
/////////////////////////
|
||||
if (upload && upload.length > 0) {
|
||||
//insert Each of the documents?
|
||||
upload.forEach((u) => {
|
||||
handleUpload(
|
||||
{ file: u.originFileObj },
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
///////////////////////////
|
||||
setLoading(false);
|
||||
notification["success"]({
|
||||
message: t("bills.successes.created"),
|
||||
});
|
||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ billlines: [] });
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
setEnterAgain(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -183,7 +258,10 @@ function BillEnterModalContainer({
|
||||
0,
|
||||
}}
|
||||
>
|
||||
<BillFormContainer form={form} />
|
||||
<BillFormContainer
|
||||
form={form}
|
||||
disableInvNumber={billEnterModal.context.disableInvNumber}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Upload,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -18,12 +18,14 @@ import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.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";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import BillFormLines from "./bill-form.lines.component";
|
||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
@@ -38,6 +40,7 @@ export function BillFormComponent({
|
||||
responsibilityCenters,
|
||||
loadLines,
|
||||
billEdit,
|
||||
disableInvNumber,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -47,7 +50,6 @@ export function BillFormComponent({
|
||||
setDiscount(opt.discount);
|
||||
};
|
||||
|
||||
//TODO: Test this further. Required to set discount when viewing an invoice.
|
||||
useEffect(() => {
|
||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
||||
const vendorId = form.getFieldValue("vendorid");
|
||||
@@ -65,7 +67,15 @@ export function BillFormComponent({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LayoutFormRow>
|
||||
<FormFieldsChanged form={form} />
|
||||
<Form.Item
|
||||
style={{ display: "none" }}
|
||||
name="isinhouse"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("bills.fields.ro_number")}
|
||||
@@ -78,6 +88,8 @@ export function BillFormComponent({
|
||||
>
|
||||
<JobSearchSelect
|
||||
disabled={billEdit || disabled}
|
||||
convertedOnly
|
||||
// notExported={false}
|
||||
onBlur={() => {
|
||||
if (form.getFieldValue("jobid") !== null) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
@@ -103,7 +115,6 @@ export function BillFormComponent({
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("bills.fields.invoice_number")}
|
||||
@@ -118,7 +129,7 @@ export function BillFormComponent({
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const vendorid = getFieldValue("vendorid");
|
||||
if (vendorid) {
|
||||
if (vendorid && value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_BILL_INVOICE_NUMBER,
|
||||
variables: {
|
||||
@@ -129,6 +140,12 @@ export function BillFormComponent({
|
||||
|
||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.bills_aggregate.nodes.length === 1 &&
|
||||
response.data.bills_aggregate.nodes[0].id ===
|
||||
form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("bills.validation.unique_invoice_number")
|
||||
@@ -140,8 +157,9 @@ export function BillFormComponent({
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bills.fields.date")}
|
||||
name="date"
|
||||
@@ -191,9 +209,8 @@ export function BillFormComponent({
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled}>
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
@@ -212,7 +229,6 @@ export function BillFormComponent({
|
||||
responsibilityCenters={responsibilityCenters}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="upload"
|
||||
label="Upload"
|
||||
@@ -229,7 +245,6 @@ export function BillFormComponent({
|
||||
<Button>Click to upload</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const values = form.getFieldsValue([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLazyQuery, useQuery } from "@apollo/react-hooks";
|
||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -11,7 +11,13 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
|
||||
export function BillFormContainer({
|
||||
bodyshop,
|
||||
form,
|
||||
billEdit,
|
||||
disabled,
|
||||
disableInvNumber,
|
||||
}) {
|
||||
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
|
||||
|
||||
const [loadLines, { data: lineData }] = useLazyQuery(
|
||||
@@ -29,6 +35,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,24 @@ import {
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
export default function BillEnterModalLinesComponent({
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function BillEnterModalLinesComponent({
|
||||
bodyshop,
|
||||
disabled,
|
||||
lineData,
|
||||
discount,
|
||||
@@ -22,7 +35,7 @@ export default function BillEnterModalLinesComponent({
|
||||
responsibilityCenters,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { setFieldsValue, getFieldsValue } = form;
|
||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||
|
||||
return (
|
||||
<Form.List name="billlines">
|
||||
@@ -104,7 +117,7 @@ export default function BillEnterModalLinesComponent({
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.actual")}
|
||||
label={t("billlines.fields.actual_price")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
rules={[
|
||||
@@ -127,8 +140,12 @@ export default function BillEnterModalLinesComponent({
|
||||
...item,
|
||||
actual_cost: !!item.actual_cost
|
||||
? item.actual_cost
|
||||
: parseFloat(e.target.value) *
|
||||
(1 - discount),
|
||||
: Math.round(
|
||||
(parseFloat(e.target.value) *
|
||||
(1 - discount) +
|
||||
Number.EPSILON) *
|
||||
100
|
||||
) / 100,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
@@ -212,6 +229,125 @@ export default function BillEnterModalLinesComponent({
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.labels.deductfromlabor")}
|
||||
key={`${index}deductfromlabor`}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "deductfromlabor"]}
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) =>
|
||||
prev.billlines[index] &&
|
||||
prev.billlines[index].deductfromlabor !==
|
||||
cur.billlines[index] &&
|
||||
cur.billlines[index].deductfromlabor
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
if (
|
||||
getFieldValue([
|
||||
"billlines",
|
||||
field.name,
|
||||
"deductfromlabor",
|
||||
])
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
key={`${index}modlbrty`}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={[
|
||||
field.name,
|
||||
"lbr_adjustment",
|
||||
"mod_lbr_ty",
|
||||
]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">
|
||||
{t("joblines.fields.lbr_types.LAA")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAB">
|
||||
{t("joblines.fields.lbr_types.LAB")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAD">
|
||||
{t("joblines.fields.lbr_types.LAD")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAE">
|
||||
{t("joblines.fields.lbr_types.LAE")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAF">
|
||||
{t("joblines.fields.lbr_types.LAF")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAG">
|
||||
{t("joblines.fields.lbr_types.LAG")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAM">
|
||||
{t("joblines.fields.lbr_types.LAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAR">
|
||||
{t("joblines.fields.lbr_types.LAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAS">
|
||||
{t("joblines.fields.lbr_types.LAS")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAU">
|
||||
{t("joblines.fields.lbr_types.LAU")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA1">
|
||||
{t("joblines.fields.lbr_types.LA1")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA2">
|
||||
{t("joblines.fields.lbr_types.LA2")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA3">
|
||||
{t("joblines.fields.lbr_types.LA3")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA4">
|
||||
{t("joblines.fields.lbr_types.LA4")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.labels.adjustmentrate")}
|
||||
name={[field.name, "lbr_adjustment", "rate"]}
|
||||
initialValue={
|
||||
bodyshop.default_adjustment_rate
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={2} min={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return <span />;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.location")}
|
||||
key={`${index}location`}
|
||||
name={[field.name, "location"]}
|
||||
>
|
||||
<Select style={{ width: "10rem" }} disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
@@ -246,3 +382,8 @@ export default function BillEnterModalLinesComponent({
|
||||
</Form.List>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillEnterModalLinesComponent);
|
||||
|
||||
@@ -11,6 +11,7 @@ export const CalculateBillTotal = (invoice) => {
|
||||
local_tax_rate,
|
||||
state_tax_rate,
|
||||
} = invoice;
|
||||
|
||||
//TODO Determine why this recalculates so many times.
|
||||
let subtotal = Dinero({ amount: 0 });
|
||||
let federalTax = Dinero({ amount: 0 });
|
||||
@@ -22,8 +23,10 @@ export const CalculateBillTotal = (invoice) => {
|
||||
billlines.forEach((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({
|
||||
amount: Math.round((i.actual_cost || 0) * 100) || 0,
|
||||
amount:
|
||||
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
|
||||
}).multiply(i.quantity || 1);
|
||||
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
if (i.applicable_taxes.federal) {
|
||||
federalTax = federalTax.add(
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Checkbox, Descriptions, Input, Table, Typography } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Input,
|
||||
Space,
|
||||
Table,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -10,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//jobRO: selectJobReadOnly,
|
||||
@@ -106,7 +115,7 @@ export function BillsListTableComponent({
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Space>
|
||||
{record.exported ? (
|
||||
<Button disabled>{t("bills.actions.edit")}</Button>
|
||||
) : (
|
||||
@@ -116,7 +125,8 @@ export function BillsListTableComponent({
|
||||
<Button>{t("bills.actions.edit")}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<BillDeleteButton bill={record} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -136,7 +146,7 @@ export function BillsListTableComponent({
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.retail"),
|
||||
title: t("billlines.fields.actual_price"),
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
||||
@@ -248,8 +258,7 @@ export function BillsListTableComponent({
|
||||
!selectedBillLinesByBill[record.id] ||
|
||||
(selectedBillLinesByBill[record.id] &&
|
||||
selectedBillLinesByBill[record.id].length === 0) ||
|
||||
record.is_credit_memo ||
|
||||
record.exported
|
||||
record.is_credit_memo
|
||||
}
|
||||
onClick={() =>
|
||||
setPartsOrderContext({
|
||||
@@ -266,8 +275,8 @@ export function BillsListTableComponent({
|
||||
.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
db_price: i.actual_price,
|
||||
act_price: i.actual_cost,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { Table, Input } from "antd";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSubscription } from "@apollo/react-hooks";
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -11,6 +11,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
chatVisible: selectChatVisible,
|
||||
@@ -27,6 +28,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge, List, Tag } from "antd";
|
||||
import { Badge, List, Tag, Tooltip } from "antd";
|
||||
import { AlertFilled } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -6,6 +7,7 @@ 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";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
@@ -21,6 +23,8 @@ export function ChatConversationListComponent({
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<List
|
||||
@@ -39,11 +43,19 @@ export function ChatConversationListComponent({
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<div className="chat-name">
|
||||
{item.job_conversations.map((j, idx) => (
|
||||
<span key={idx}>
|
||||
{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
<div key={idx} style={{ display: "flex" }}>
|
||||
{j.job.owner && !j.job.owner.allow_text_message && (
|
||||
<Tooltip title={t("messaging.labels.noallowtxt")}>
|
||||
<AlertFilled
|
||||
className="production-alert"
|
||||
style={{ marginRight: ".3rem", alignItems: "center" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div>{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} `}
|
||||
</span>
|
||||
} `}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Tag } from "antd";
|
||||
import React from "react";
|
||||
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";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
|
||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useSubscription } from "@apollo/react-hooks";
|
||||
import { useMutation, useSubscription } from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { PictureFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Badge, Popover } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||
|
||||
export function ChatMediaSelector({
|
||||
selectedMedia,
|
||||
setSelectedMedia,
|
||||
conversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
console.log("conversation", conversation);
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
variables: {
|
||||
jobId:
|
||||
conversation.job_conversations[0] &&
|
||||
conversation.job_conversations[0].jobid,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
skip:
|
||||
!conversation.job_conversations ||
|
||||
conversation.job_conversations.length === 0,
|
||||
});
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const handleVisibleChange = (visible) => {
|
||||
setVisible(visible);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMedia([]);
|
||||
}, [setSelectedMedia, conversation]);
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
{loading && <LoadingSpinner />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
conversation.job_conversations.length === 0 ? (
|
||||
<div>{t("messaging.errors.noattachedjobs")}</div>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
title={t("messaging.labels.selectmedia")}
|
||||
trigger="click"
|
||||
visible={visible}
|
||||
onVisibleChange={handleVisibleChange}
|
||||
>
|
||||
<Badge
|
||||
size="small"
|
||||
count={selectedMedia.filter((s) => s.isSelected).length}
|
||||
>
|
||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||
</Badge>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||
import {
|
||||
@@ -44,6 +46,16 @@ export default function ChatMessageListComponent({ messages }) {
|
||||
{MessageRender(messages[index])}
|
||||
{StatusRender(messages[index].status)}
|
||||
</div>
|
||||
{messages[index].isoutbound && (
|
||||
<div style={{ fontSize: 10 }}>
|
||||
{i18n.t("messaging.labels.sentby", {
|
||||
by: messages[index].userid,
|
||||
time: moment(messages[index].created_at).format(
|
||||
"MM/DD/YYYY @ hh:mm a"
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
@@ -72,15 +84,19 @@ export default function ChatMessageListComponent({ messages }) {
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{message.image_path &&
|
||||
message.image_path.map((i, idx) => (
|
||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||
<a href={i} target="__blank">
|
||||
<img alt="Received" className="message-img" src={i} />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusRender = (status) => {
|
||||
|
||||
@@ -34,9 +34,10 @@
|
||||
//display: inline-block;
|
||||
|
||||
.message-img {
|
||||
max-width: 3rem;
|
||||
max-height: 3rem;
|
||||
max-width: 10rem;
|
||||
max-height: 10rem;
|
||||
object-fit: contain;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormItem from "../form-items-formatted/phone-form-item.component";
|
||||
import PhoneFormItem, {
|
||||
PhoneItemFormatterValidation,
|
||||
} from "../form-items-formatted/phone-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -26,7 +28,14 @@ export function ChatNewConversation({ openChatByPhone }) {
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<Form.Item label={t("messaging.labels.phonenumber")} name="phoneNumber">
|
||||
<Form.Item
|
||||
label={t("messaging.labels.phonenumber")}
|
||||
name="phoneNumber"
|
||||
rules={[
|
||||
({ getFieldValue }) =>
|
||||
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
|
||||
]}
|
||||
>
|
||||
<PhoneFormItem />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
selectMessage,
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -36,6 +37,7 @@ function ChatSendMessageComponent({
|
||||
setMessage,
|
||||
}) {
|
||||
const inputArea = useRef(null);
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
@@ -43,36 +45,55 @@ function ChatSendMessageComponent({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
if (message === "" || !message) return;
|
||||
logImEXEvent("messaging_send_message");
|
||||
sendMessage({
|
||||
to: conversation.phone_num,
|
||||
body: message,
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
});
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if (selectedImages < 11) {
|
||||
sendMessage({
|
||||
to: conversation.phone_num,
|
||||
body: message,
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
});
|
||||
setSelectedMedia(
|
||||
selectedMedia.map((i) => {
|
||||
return { ...i, isSelected: false };
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row">
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
autoFocus
|
||||
ref={inputArea}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={message}
|
||||
disabled={isSending}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
<ChatMediaSelector
|
||||
conversation={conversation}
|
||||
selectedMedia={selectedMedia}
|
||||
setSelectedMedia={setSelectedMedia}
|
||||
/>
|
||||
<span style={{ flex: 1 }}>
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
autoFocus
|
||||
ref={inputArea}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={message}
|
||||
disabled={isSending}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<SendOutlined
|
||||
className="imex-flex-row__margin"
|
||||
disabled={message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
|
||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
indicator={
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
|
||||
@@ -3,11 +3,12 @@ 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 {
|
||||
const e = {
|
||||
checkbox: CheckboxFormItem,
|
||||
slider: Slider,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
rate: Rate,
|
||||
};
|
||||
|
||||
export default e;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -14,6 +14,7 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
|
||||
form.setFieldsValue({
|
||||
kmstart: record.mileage,
|
||||
dailyrate: record.dailycost,
|
||||
fuelout: record.fuel,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, notification, Popover, Radio, Form, InputNumber } from "antd";
|
||||
import {
|
||||
Button,
|
||||
notification,
|
||||
Popover,
|
||||
Radio,
|
||||
Form,
|
||||
InputNumber,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "react-apollo";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||
import moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
@@ -11,6 +19,8 @@ import {
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -20,7 +30,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
export function ContractConvertToRo({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
contract,
|
||||
disabled,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -46,6 +61,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
part_type: "CCDR",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
db_ref: "io-ccdr",
|
||||
// mod_lbr_ty: "PAL",
|
||||
},
|
||||
];
|
||||
@@ -63,6 +79,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
part_type: "CCM",
|
||||
part_qty: mileageDiff,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccm",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
@@ -78,6 +95,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
part_qty: values.refuelqty,
|
||||
part_type: "CCF",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccf",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
@@ -92,6 +110,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
part_qty: 1,
|
||||
part_type: "CCC",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccc",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
@@ -107,6 +126,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
part_type: "CCD",
|
||||
part_qty: 1,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccd",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
@@ -119,8 +139,9 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||
clm_no: `${contract.job.clm_no}-CC`,
|
||||
clm_total: 1234, //TODO
|
||||
ins_co_nm: "CC",
|
||||
converted: true,
|
||||
clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
|
||||
ownr_fn: contract.job.owner.ownr_fn,
|
||||
ownr_ln: contract.job.owner.ownr_ln,
|
||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||
@@ -145,13 +166,69 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
data: billingLines,
|
||||
},
|
||||
parts_tax_rates: {
|
||||
PAA: {
|
||||
prt_type: "PAA",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAC: {
|
||||
prt_type: "PAC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAL: {
|
||||
prt_type: "PAL",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAM: {
|
||||
prt_type: "PAM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAN: {
|
||||
prt_type: "PAN",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAR: {
|
||||
prt_type: "PAR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAS: {
|
||||
prt_type: "PAS",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCDR: {
|
||||
prt_type: "CCDR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCF: {
|
||||
prt_type: "CCF",
|
||||
@@ -159,7 +236,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCM: {
|
||||
prt_type: "CCM",
|
||||
@@ -167,7 +244,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCC: {
|
||||
prt_type: "CCC",
|
||||
@@ -175,7 +252,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCD: {
|
||||
prt_type: "CCD",
|
||||
@@ -183,10 +260,22 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: 0.07,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
//Calcualte the new job totals.
|
||||
|
||||
const newTotals = (
|
||||
await axios.post("/job/totals", {
|
||||
job: { ...newJob, joblines: billingLines },
|
||||
})
|
||||
).data;
|
||||
|
||||
newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
|
||||
newJob.job_totals = newTotals;
|
||||
|
||||
const result = await insertJob({
|
||||
variables: { job: [newJob] },
|
||||
// refetchQueries: ["GET_JOB_BY_PK"],
|
||||
@@ -244,12 +333,14 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
>
|
||||
<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>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
@@ -257,7 +348,11 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button onClick={() => setVisible(true)} loading={loading}>
|
||||
<Button
|
||||
onClick={() => setVisible(true)}
|
||||
loading={loading}
|
||||
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
|
||||
>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
</Popover>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GET_JOB_FOR_CC_CONTRACT } from "../../graphql/jobs.queries";
|
||||
export default function ContractCreateJobPrefillComponent({ jobId, form }) {
|
||||
const [call, { loading, error, data }] = useLazyQuery(
|
||||
GET_JOB_FOR_CC_CONTRACT
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
call({ variables: { id: jobId } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
driver_dlst: data.jobs_by_pk.ownr_ast,
|
||||
driver_fn: data.jobs_by_pk.ownr_fn,
|
||||
driver_ln: data.jobs_by_pk.ownr_ln,
|
||||
driver_addr1: data.jobs_by_pk.ownr_addr1,
|
||||
driver_state: data.jobs_by_pk.ownr_st,
|
||||
driver_city: data.jobs_by_pk.ownr_city,
|
||||
driver_zip: data.jobs_by_pk.ownr_zip,
|
||||
driver_ph1: data.jobs_by_pk.ownr_ph1,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
if (error) {
|
||||
notification["error"]({
|
||||
message: t("contracts.errors.fetchingjobinfo", {
|
||||
error: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} disabled={!jobId} loading={loading}>
|
||||
{t("contracts.labels.populatefromjob")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,20 @@ import { Form, Input, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-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 InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
||||
|
||||
export default function ContractFormComponent({ form, create = false }) {
|
||||
import InputPhone, {
|
||||
PhoneItemFormatterValidation,
|
||||
} from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import ContractFormJobPrefill from "./contract-form-job-prefill.component";
|
||||
export default function ContractFormComponent({
|
||||
form,
|
||||
create = false,
|
||||
selectedJobState,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -46,12 +53,6 @@ export default function ContractFormComponent({ form, create = false }) {
|
||||
<Form.Item
|
||||
label={t("contracts.fields.scheduledreturn")}
|
||||
name="scheduledreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
@@ -84,6 +85,33 @@ export default function ContractFormComponent({ form, create = false }) {
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelout")}
|
||||
name="fuelout"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.fuelin")} name="fuelin">
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
{selectedJobState && (
|
||||
<div>
|
||||
<ContractFormJobPrefill
|
||||
jobId={selectedJobState && selectedJobState[0]}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
@@ -163,40 +191,16 @@ export default function ContractFormComponent({ form, create = false }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_city")}
|
||||
name="driver_city"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Form.Item label={t("contracts.fields.driver_city")} name="driver_city">
|
||||
<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"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Form.Item label={t("contracts.fields.driver_zip")} name="driver_zip">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -207,62 +211,17 @@ export default function ContractFormComponent({ form, create = false }) {
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) =>
|
||||
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
|
||||
]}
|
||||
>
|
||||
<InputPhone />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dob")}
|
||||
name="driver_dob"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
|
||||
<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} />
|
||||
|
||||
@@ -23,17 +23,13 @@ export default function ContractsJobsComponent({
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.ro_number ? a.ro_number : "EST-" + a.est_number,
|
||||
b.ro_number ? b.ro_number : "EST-" + b.est_number
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
|
||||
{record.ro_number ? record.ro_number : t("general.labels.na")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -135,10 +131,6 @@ export default function ContractsJobsComponent({
|
||||
? data
|
||||
: data.filter(
|
||||
(j) =>
|
||||
(j.est_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ro_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
@@ -62,9 +62,11 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
|
||||
//sortOrder:
|
||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={`/manage/courtesycars/${record.courtesycar.id}`}
|
||||
>{`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`}</Link>
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model} ${
|
||||
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
|
||||
}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ 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';
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
|
||||
export default function CourtesyCarReturnModalComponent() {
|
||||
const { t } = useTranslation();
|
||||
@@ -15,8 +15,8 @@ export default function CourtesyCarReturnModalComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
@@ -27,20 +27,20 @@ export default function CourtesyCarReturnModalComponent() {
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name={["courtesycar", "data", "fuel"]}
|
||||
label={t("contracts.fields.fuelin")}
|
||||
name={"fuelin"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required")
|
||||
}
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
|
||||
@@ -9,7 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
|
||||
import moment from "moment";
|
||||
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useMutation } from "@apollo/client";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
courtesyCarReturnModal: selectCourtesyCarReturn,
|
||||
@@ -39,11 +39,12 @@ export function BillEnterModalContainer({
|
||||
kmend: values.kmend,
|
||||
actualreturn: values.actualreturn,
|
||||
status: "contracts.status.returned",
|
||||
fuelin: values.fuelin,
|
||||
},
|
||||
courtesycarid: context.courtesyCarId,
|
||||
courtesycar: {
|
||||
status: "courtesycars.status.in",
|
||||
fuel: values.fuel,
|
||||
fuel: values.fuelin,
|
||||
mileage: values.kmend,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarStatusComponent = (
|
||||
{ value = "courtesycars.status.in", onChange },
|
||||
ref
|
||||
) => {
|
||||
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -75,6 +75,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.labels.outwith"),
|
||||
dataIndex: "outwith",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Form } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
@@ -31,21 +31,9 @@ export default function CsiResponseListPaginated({
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
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",
|
||||
width: "8%",
|
||||
sorter: (a, b) => a.job.est_number - b.job.est_number,
|
||||
sortOrder: sortcolumn === "est_number" && sortorder,
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.est_number}
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// import Icon from "@ant-design/icons";
|
||||
// import { Button, Dropdown, Menu, notification } from "antd";
|
||||
// import React, { useState } from "react";
|
||||
// import { useMutation, useQuery } from "react-apollo";
|
||||
// import { useMutation, useQuery } from "@apollo/client";
|
||||
// import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { MdClose } from "react-icons/md";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { Button, Upload } from "antd";
|
||||
import { Upload } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -15,6 +15,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
export function DocumentsUploadComponent({
|
||||
children,
|
||||
currentUser,
|
||||
bodyshop,
|
||||
jobId,
|
||||
@@ -23,7 +24,7 @@ export function DocumentsUploadComponent({
|
||||
callbackAfterUpload,
|
||||
}) {
|
||||
return (
|
||||
<Upload
|
||||
<Upload.Dragger
|
||||
multiple={true}
|
||||
customRequest={(ev) =>
|
||||
handleUpload(ev, {
|
||||
@@ -36,12 +37,24 @@ export function DocumentsUploadComponent({
|
||||
})
|
||||
}
|
||||
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||
showUploadList={false}
|
||||
// showUploadList={false}
|
||||
>
|
||||
<Button type="primary">
|
||||
<UploadOutlined />
|
||||
</Button>
|
||||
</Upload>
|
||||
{
|
||||
// <Button type="primary">
|
||||
// <UploadOutlined />
|
||||
// </Button>
|
||||
}
|
||||
{children || (
|
||||
<>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
Click or drag files to this area to upload.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Upload.Dragger>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(DocumentsUploadComponent);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import i18n from "i18next";
|
||||
import client, { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
@@ -19,8 +20,10 @@ export const handleUpload = (ev, context) => {
|
||||
const { bodyshop, jobId } = context;
|
||||
|
||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
||||
let extension = ev.file.name.split(".").pop();
|
||||
uploadToCloudinary(
|
||||
key,
|
||||
extension,
|
||||
ev.file.type,
|
||||
ev.file,
|
||||
onError,
|
||||
@@ -32,6 +35,7 @@ export const handleUpload = (ev, context) => {
|
||||
|
||||
export const uploadToCloudinary = async (
|
||||
key,
|
||||
extension,
|
||||
fileType,
|
||||
file,
|
||||
onError,
|
||||
@@ -50,12 +54,16 @@ export const uploadToCloudinary = async (
|
||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
|
||||
//Get the signed url.
|
||||
console.log("fileType", fileType);
|
||||
const upload_preset = fileType.startsWith("video")
|
||||
? "incoming_upload_video"
|
||||
: "incoming_upload";
|
||||
|
||||
const signedURLResponse = await axios.post("/media/sign", {
|
||||
public_id: public_id,
|
||||
tags: tags,
|
||||
timestamp: timestamp,
|
||||
upload_preset: "incoming_upload",
|
||||
upload_preset: upload_preset,
|
||||
});
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
@@ -79,8 +87,8 @@ export const uploadToCloudinary = async (
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
console.log("Applying lower quality transforms.");
|
||||
formData.append("upload_preset", "incoming_upload");
|
||||
|
||||
formData.append("upload_preset", upload_preset);
|
||||
|
||||
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
||||
formData.append("public_id", public_id);
|
||||
@@ -88,9 +96,10 @@ export const uploadToCloudinary = async (
|
||||
formData.append("timestamp", timestamp);
|
||||
formData.append("signature", signature);
|
||||
|
||||
//Upload request to Cloudinary
|
||||
const cloudinaryUploadResponse = await cleanAxios.post(
|
||||
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
|
||||
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(
|
||||
fileType
|
||||
)}/upload`,
|
||||
formData,
|
||||
{
|
||||
...options,
|
||||
@@ -118,11 +127,13 @@ export const uploadToCloudinary = async (
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
jobid: jobId,
|
||||
...(jobId ? { jobid: jobId } : {}),
|
||||
...(billId ? { billid: billId } : {}),
|
||||
uploaded_by: uploaded_by,
|
||||
key: key,
|
||||
billid: billId,
|
||||
type: fileType,
|
||||
extension: extension,
|
||||
bodyshopid: bodyshop.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -145,9 +156,19 @@ export const uploadToCloudinary = async (
|
||||
if (!!onError) onError(JSON.stringify(documentInsert.errors));
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: JSON.stringify(JSON.stringify(documentInsert.errors)),
|
||||
message: JSON.stringify(documentInsert.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export function DetermineFileType(filetype) {
|
||||
if (!filetype) return "auto";
|
||||
else if (filetype.startsWith("image")) return "image";
|
||||
else if (filetype.startsWith("video")) return "video";
|
||||
else if (filetype.startsWith("application/pdf")) return "image";
|
||||
else if (filetype.startsWith("application")) return "raw";
|
||||
|
||||
return "auto";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
import { Button, Input, Upload, Select } from "antd";
|
||||
import { Button, Card, Input, Select, Upload } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function EmailOverlayComponent({
|
||||
messageOptions,
|
||||
@@ -11,6 +12,7 @@ export default function EmailOverlayComponent({
|
||||
handleUpload,
|
||||
handleFileRemove,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
To:
|
||||
@@ -18,22 +20,28 @@ export default function EmailOverlayComponent({
|
||||
name="to"
|
||||
mode="tags"
|
||||
value={messageOptions.to}
|
||||
style={{ width: "100%" }}
|
||||
//style={{ width: "100%" }}
|
||||
onChange={handleToChange}
|
||||
tokenSeparators={[",", ";"]}
|
||||
/>
|
||||
CC:
|
||||
<Input
|
||||
<Select
|
||||
value={messageOptions.cc}
|
||||
onChange={handleConfigChange}
|
||||
mode="tags"
|
||||
onChange={(value) => handleConfigChange("cc", value)}
|
||||
name="cc"
|
||||
tokenSeparators={[",", ";"]}
|
||||
/>
|
||||
Subject:
|
||||
<Input
|
||||
value={messageOptions.subject}
|
||||
onChange={handleConfigChange}
|
||||
onChange={(e) => handleConfigChange("subject", e.target.value)}
|
||||
name="subject"
|
||||
/>
|
||||
<div style={{ color: "red" }}>
|
||||
DEVELOPER NOTE: Any edits made in the editor below will not be sent or
|
||||
saved due to css inlining issues.
|
||||
</div>
|
||||
<Editor
|
||||
value={messageOptions.html}
|
||||
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
|
||||
@@ -53,17 +61,20 @@ export default function EmailOverlayComponent({
|
||||
}}
|
||||
onEditorChange={handleHtmlChange}
|
||||
/>
|
||||
<Upload
|
||||
fileList={messageOptions.fileList}
|
||||
beforeUpload={handleUpload}
|
||||
onRemove={handleFileRemove}
|
||||
multiple
|
||||
listType="picture-card"
|
||||
>
|
||||
<Button>
|
||||
<UploadOutlined /> Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
<Card title={t("emails.labels.attachments")}>
|
||||
<Upload
|
||||
fileList={messageOptions.fileList}
|
||||
beforeUpload={handleUpload}
|
||||
onRemove={handleFileRemove}
|
||||
multiple
|
||||
listType="picture-card"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Button>
|
||||
<UploadOutlined /> Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export function EmailOverlayContainer({
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [rawHtml, setRawHtml] = useState("");
|
||||
const defaultEmailFrom = {
|
||||
from: {
|
||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
||||
@@ -65,7 +66,11 @@ export function EmailOverlayContainer({
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
await axios.post("/sendemail", { ...messageOptions, attachments });
|
||||
await axios.post("/sendemail", {
|
||||
...messageOptions,
|
||||
html: rawHtml,
|
||||
attachments,
|
||||
});
|
||||
notification["success"]({ message: t("emails.successes.sent") });
|
||||
toggleEmailOverlayVisible();
|
||||
} catch (error) {
|
||||
@@ -77,8 +82,7 @@ export function EmailOverlayContainer({
|
||||
setSending(false);
|
||||
};
|
||||
|
||||
const handleConfigChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
const handleConfigChange = (name, value) => {
|
||||
setMessageOptions({ ...messageOptions, [name]: value });
|
||||
};
|
||||
const handleHtmlChange = (text) => {
|
||||
@@ -109,11 +113,19 @@ export function EmailOverlayContainer({
|
||||
const render = async () => {
|
||||
logImEXEvent("email_render_template", { template: emailConfig.template });
|
||||
setLoading(true);
|
||||
let html = await RenderTemplate(emailConfig.template, bodyshop);
|
||||
let html = await RenderTemplate(emailConfig.template, bodyshop, true);
|
||||
|
||||
const response = await axios.post("/render/inlinecss", {
|
||||
html: html,
|
||||
url: `${window.location.protocol}://${window.location.host}/`,
|
||||
});
|
||||
setRawHtml(response.data);
|
||||
|
||||
console.log("response", response);
|
||||
setMessageOptions({
|
||||
...emailConfig.messageOptions,
|
||||
...defaultEmailFrom,
|
||||
html: html,
|
||||
html: response.data,
|
||||
fileList: [],
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
104
client/src/components/email-test/email-test-component.jsx
Normal file
104
client/src/components/email-test/email-test-component.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Button, Form, Input, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
});
|
||||
|
||||
export function EmailTestComponent({ currentUser, setEmailOptions }) {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFinish = (values) => {
|
||||
console.log("values", values);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: values.key,
|
||||
variables: {
|
||||
...(values.id ? { id: values.id } : {}),
|
||||
...(values.start ? { start: values.start } : {}),
|
||||
...(values.end ? { end: values.end } : {}),
|
||||
},
|
||||
},
|
||||
{
|
||||
to: values.to,
|
||||
},
|
||||
values.email ? "e" : "p"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={{
|
||||
to: [
|
||||
"allan.carr@thinkimex.com",
|
||||
"allanlcarr@outlook.com",
|
||||
"allanlcarr@icloud.com",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
name="to"
|
||||
label="Recipients"
|
||||
rules={[
|
||||
{
|
||||
type: "array",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="Template Key"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="id" label="Record ID">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Generate as email?"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item name="start" label="Start Date">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item name="end" label="End Date">
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
<Button onClick={() => form.submit()}>Execute</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EmailTestComponent);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Select, Tag } from "antd";
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import React, { forwardRef, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
@@ -35,18 +35,16 @@ const EmployeeSearchSelect = (
|
||||
key={o.id}
|
||||
value={o.id}
|
||||
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
discount={o.discount}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
<Tag color="blue">{o.cost_center}</Tag>
|
||||
|
||||
<Tag color="green">
|
||||
{o.flat_rate
|
||||
? t("timetickets.labels.flat_rate")
|
||||
: t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { withApollo } from "@apollo/client/react/hoc";
|
||||
import React, { Component } from "react";
|
||||
import { withApollo } from "react-apollo";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_FCM_TOKEN } from "../../graphql/user.queries";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -15,17 +14,17 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
class FcmNotificationComponent extends Component {
|
||||
async componentDidMount() {
|
||||
const { client, currentUser } = this.props;
|
||||
//const { client, currentUser } = this.props;
|
||||
if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
|
||||
|
||||
messaging
|
||||
.requestPermission()
|
||||
.then(async function () {
|
||||
const token = await messaging.getToken();
|
||||
client.mutate({
|
||||
mutation: UPDATE_FCM_TOKEN,
|
||||
variables: { authEmail: currentUser.email, token: { [token]: true } },
|
||||
});
|
||||
// const token = await messaging.getToken();
|
||||
// client.mutate({
|
||||
// mutation: UPDATE_FCM_TOKEN,
|
||||
// variables: { authEmail: currentUser.email, token: { [token]: true } },
|
||||
// });
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log("Unable to get permission to notify.", err);
|
||||
@@ -44,22 +43,19 @@ export default connect(
|
||||
)(withApollo(FcmNotificationComponent));
|
||||
|
||||
//Firebase Service Worker Register
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/firebase-messaging-sw.js")
|
||||
.then(function (registration) {
|
||||
console.log(
|
||||
"[FCM] Registration successful, scope is:",
|
||||
registration.scope
|
||||
);
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
console.log("Handler for Navigator Service Worker.", event);
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log(
|
||||
"[FCM] Service worker registration failed, error:",
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
// if ("serviceWorker" in navigator) {
|
||||
// navigator.serviceWorker
|
||||
// .register("/firebase-messaging-sw.js")
|
||||
// .then(function (registration) {
|
||||
// console.log(
|
||||
// "[FCM] Registration successful, scope is:",
|
||||
// registration.scope
|
||||
// );
|
||||
// navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
// console.log("Handler for Navigator Service Worker.", event);
|
||||
// });
|
||||
// })
|
||||
// .catch(function (err) {
|
||||
// console.log("[FCM] Service worker registration failed, error:", err);
|
||||
// });
|
||||
// }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function FooterComponent() {
|
||||
return (
|
||||
<Row>
|
||||
<Col span={8} offset={8}>
|
||||
Copyright Snapt Software 2019. All rights reserved.
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { TimePicker } from "antd";
|
||||
import moment from "moment";
|
||||
//To be used as a form element only.
|
||||
|
||||
const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
||||
const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
|
||||
// const handleChange = (newDate) => {
|
||||
// if (value !== newDate && onChange) {
|
||||
// onChange(newDate);
|
||||
@@ -14,7 +14,7 @@ const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
||||
// };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id={id}>
|
||||
<FormDatePicker
|
||||
{...restProps}
|
||||
value={value}
|
||||
|
||||
@@ -12,7 +12,6 @@ const FormInputNUmberCalculator = (
|
||||
const ref = useRef(null);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
console.log("e :>> ", e.currentTarget.value);
|
||||
const { key } = e;
|
||||
let action;
|
||||
switch (key) {
|
||||
|
||||
@@ -1,7 +1,50 @@
|
||||
import i18n from "i18next";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React, { forwardRef } from "react";
|
||||
import NumberFormat from "react-number-format";
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
import "react-phone-input-2/lib/high-res.css";
|
||||
import "./phone-form-item.styles.scss";
|
||||
|
||||
function FormItemPhone(props, ref) {
|
||||
return <NumberFormat {...props} ref={ref} type="tel" format="###-###-####" />;
|
||||
return (
|
||||
<PhoneInput
|
||||
country="ca"
|
||||
onlyCountries={["ca", "us"]}
|
||||
ref={ref}
|
||||
className="ant-input"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(FormItemPhone);
|
||||
|
||||
export const PhoneItemFormatterValidation = (getFieldValue, name) => ({
|
||||
async validator(rule, value) {
|
||||
if (!getFieldValue(name)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const p = parsePhoneNumber(getFieldValue(name), "CA");
|
||||
|
||||
if (p) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(i18n.t("general.validation.invalidphone"));
|
||||
}
|
||||
}
|
||||
|
||||
// PhoneInput({
|
||||
// value: getFieldValue(name),
|
||||
// isValid: async (value, country) => {
|
||||
// console.log("value", value);
|
||||
// if (value.match(/12345/)) {
|
||||
|
||||
// } else if (value.match(/1234/)) {
|
||||
// return false;
|
||||
// } else {
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.react-tel-input .form-control {
|
||||
width: 100% !important;
|
||||
height: unset !important;
|
||||
border-radius: unset !important;
|
||||
line-height: unset !important;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { UpOutlined, DownOutlined } from "@ant-design/icons";
|
||||
export default function FormListMoveArrows({ move, index, total }) {
|
||||
const upDisabled = index === 0;
|
||||
const downDisabled = index === total - 1;
|
||||
@@ -9,7 +9,7 @@ export default function FormListMoveArrows({ move, index, total }) {
|
||||
};
|
||||
|
||||
const handleDown = () => {
|
||||
move(index, index - 1);
|
||||
move(index, index + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useLazyQuery } from "@apollo/react-hooks";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { AutoComplete, Input } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -14,27 +15,18 @@ export default function GlobalSearch() {
|
||||
GLOBAL_SEARCH_QUERY
|
||||
);
|
||||
|
||||
const handleSearch = (searchTerm) => {
|
||||
logImEXEvent("global_search", { term: searchTerm });
|
||||
const executeSearch = (v) => {
|
||||
callSearch(v);
|
||||
};
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
if (searchTerm.length > 0)
|
||||
callSearch({ variables: { search: searchTerm } });
|
||||
const handleSearch = (value) => {
|
||||
if (value && value !== "")
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
const renderTitle = (title) => {
|
||||
return (
|
||||
<span>
|
||||
{title}
|
||||
<a
|
||||
style={{ float: "right" }}
|
||||
href="https://www.google.com/search?q=antd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
more
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
return <span>{title}</span>;
|
||||
};
|
||||
|
||||
const options = data
|
||||
@@ -48,11 +40,7 @@ export default function GlobalSearch() {
|
||||
<Link to={`/manage/jobs/${job.id}`}>
|
||||
<div className="imex-flex-row">
|
||||
<span className="imex-flex-row__margin-large">
|
||||
<strong>
|
||||
{job.ro_number
|
||||
? `${job.ro_number || ""} / ${job.est_number || ""}`
|
||||
: `${job.est_number || ""}`}
|
||||
</strong>
|
||||
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
||||
</span>
|
||||
|
||||
<span className="imex-flex-row__margin-large">{`${
|
||||
@@ -154,14 +142,13 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AutoComplete
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={{ width: 200 }}
|
||||
options={options}
|
||||
>
|
||||
<Input.Search loading={loading} onSearch={handleSearch} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
<AutoComplete
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={{ flex: 2 }}
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
>
|
||||
<Input.Search loading={loading} />
|
||||
</AutoComplete>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import Icon, {
|
||||
LineChartOutlined,
|
||||
ScheduleOutlined,
|
||||
TeamOutlined,
|
||||
ToolFilled,
|
||||
UnorderedListOutlined,
|
||||
UserOutlined,
|
||||
ToolFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { Avatar, Menu } from "antd";
|
||||
import React from "react";
|
||||
@@ -35,8 +35,6 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import "./header.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
recentItems: selectRecentItems,
|
||||
@@ -50,6 +48,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setPaymentContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setReportCenterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
|
||||
signOutStart: () => dispatch(signOutStart()),
|
||||
});
|
||||
|
||||
@@ -61,285 +61,313 @@ function Header({
|
||||
setBillEnterContext,
|
||||
setTimeTicketContext,
|
||||
setPaymentContext,
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
// style={{ backgroundColor: "#eee" }}
|
||||
className="header-main-menu"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
>
|
||||
<Menu.Item key="home">
|
||||
<Link to="/manage">
|
||||
<HomeFilled />
|
||||
{t("menus.header.home")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="schedule">
|
||||
<Link to="/manage/schedule">
|
||||
<Icon component={FaCalendarAlt} />
|
||||
{t("menus.header.schedule")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<Icon component={FaCarCrash} />
|
||||
<span>{t("menus.header.jobs")}</span>
|
||||
</span>
|
||||
}
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
style={{ flex: 5 }}
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
>
|
||||
<Menu.Item key="activejobs">
|
||||
<FileFilled />
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="parts-queue">
|
||||
<Link to="/manage/partsqueue">
|
||||
<ToolFilled /> {t("menus.header.parts-queue")}
|
||||
<Menu.Item key="home">
|
||||
<Link to="/manage">
|
||||
<HomeFilled />
|
||||
{t("menus.header.home")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="availablejobs">
|
||||
<Link to="/manage/available">
|
||||
<ImportOutlined /> {t("menus.header.availablejobs")}
|
||||
<Menu.Item key="schedule">
|
||||
<Link to="/manage/schedule">
|
||||
<Icon component={FaCalendarAlt} />
|
||||
{t("menus.header.schedule")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="alljobs">
|
||||
<UnorderedListOutlined />
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="productionlist">
|
||||
<Link to="/manage/production/list">
|
||||
<ScheduleOutlined />
|
||||
{t("menus.header.productionlist")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="productionboard">
|
||||
<Link to="/manage/production/board">
|
||||
<Icon component={BsKanban} />
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="scoreboard">
|
||||
<LineChartOutlined />
|
||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<UserOutlined />
|
||||
<span>{t("menus.header.customers")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="owners">
|
||||
<Link to="/manage/owners">
|
||||
<TeamOutlined />
|
||||
{t("menus.header.owners")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="vehicles">
|
||||
<Link to="/manage/vehicles">
|
||||
<CarFilled />
|
||||
{t("menus.header.vehicles")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<CarFilled />
|
||||
<span>{t("menus.header.courtesycars")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="courtesycarsall">
|
||||
<Link to="/manage/courtesycars">
|
||||
<CarFilled />
|
||||
{t("menus.header.courtesycars-all")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="contracts">
|
||||
<Link to="/manage/courtesycars/contracts">
|
||||
<FileFilled />
|
||||
{t("menus.header.courtesycars-contracts")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="newcontract">
|
||||
<Link to="/manage/courtesycars/contracts/new">
|
||||
<FileAddFilled />
|
||||
{t("menus.header.courtesycars-newcontract")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<DollarCircleFilled />
|
||||
<span>{t("menus.header.accounting")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="bills">
|
||||
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterbills"
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon component={FaFileInvoiceDollar} />
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="allpayments">
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
onClick={() => {
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon component={FaCreditCard} />
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="timetickets">
|
||||
<Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="entertimetickets"
|
||||
onClick={() => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.SubMenu title={t("menus.header.export")}>
|
||||
<Menu.Item key="receivables">
|
||||
<Link to="/manage/accounting/receivables">
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="payables">
|
||||
<Link to="/manage/accounting/payables">
|
||||
{t("menus.header.accounting-payables")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="payments">
|
||||
<Link to="/manage/accounting/payments">
|
||||
{t("menus.header.accounting-payments")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu title={t("menus.header.shop")}>
|
||||
<Menu.Item key="shop">
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="shop-templates">
|
||||
<Link to="/manage/shop/templates">
|
||||
{t("menus.header.shop_templates")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="shop-vendors">
|
||||
<Link to="/manage/shop/vendors">
|
||||
{t("menus.header.shop_vendors")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shop-csi">
|
||||
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Item>
|
||||
<GlobalSearch />
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</Link>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<div>
|
||||
{currentUser.photoURL ? (
|
||||
<Avatar
|
||||
src={currentUser.photoURL}
|
||||
style={{
|
||||
margin: "10px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
style={{
|
||||
backgroundColor: "#87d068",
|
||||
margin: "10px",
|
||||
}}
|
||||
icon={<UserOutlined />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser.displayName || t("general.labels.unknown")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Menu.Item danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shiftclock">
|
||||
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile">
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
<span>{t("menus.currentuser.languageselector")}</span>
|
||||
<Icon component={FaCarCrash} />
|
||||
<span>{t("menus.header.jobs")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item actiontype="lang-select" key="en-US">
|
||||
{t("general.languages.english")}
|
||||
<Menu.Item key="activejobs">
|
||||
<FileFilled />
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item actiontype="lang-select" key="fr-CA">
|
||||
{t("general.languages.french")}
|
||||
<Menu.Item key="parts-queue">
|
||||
<Link to="/manage/partsqueue">
|
||||
<ToolFilled /> {t("menus.header.parts-queue")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item actiontype="lang-select" key="es-MX">
|
||||
{t("general.languages.spanish")}
|
||||
<Menu.Item key="availablejobs">
|
||||
<Link to="/manage/available">
|
||||
<ImportOutlined /> {t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="alljobs">
|
||||
<UnorderedListOutlined />
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="productionlist">
|
||||
<Link to="/manage/production/list">
|
||||
<ScheduleOutlined />
|
||||
{t("menus.header.productionlist")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="productionboard">
|
||||
<Link to="/manage/production/board">
|
||||
<Icon component={BsKanban} />
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="scoreboard">
|
||||
<LineChartOutlined />
|
||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<UserOutlined />
|
||||
<span>{t("menus.header.customers")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="owners">
|
||||
<Link to="/manage/owners">
|
||||
<TeamOutlined />
|
||||
{t("menus.header.owners")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="vehicles">
|
||||
<Link to="/manage/vehicles">
|
||||
<CarFilled />
|
||||
{t("menus.header.vehicles")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<CarFilled />
|
||||
<span>{t("menus.header.courtesycars")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="courtesycarsall">
|
||||
<Link to="/manage/courtesycars">
|
||||
<CarFilled />
|
||||
{t("menus.header.courtesycars-all")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="contracts">
|
||||
<Link to="/manage/courtesycars/contracts">
|
||||
<FileFilled />
|
||||
{t("menus.header.courtesycars-contracts")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="newcontract">
|
||||
<Link to="/manage/courtesycars/contracts/new">
|
||||
<FileAddFilled />
|
||||
{t("menus.header.courtesycars-newcontract")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<DollarCircleFilled />
|
||||
<span>{t("menus.header.accounting")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="bills">
|
||||
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterbills"
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon component={FaFileInvoiceDollar} />
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="allpayments">
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
onClick={() => {
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon component={FaCreditCard} />
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="timetickets">
|
||||
<Link to="/manage/timetickets">
|
||||
{t("menus.header.timetickets")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="entertimetickets"
|
||||
onClick={() => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.SubMenu title={t("menus.header.export")}>
|
||||
<Menu.Item key="receivables">
|
||||
<Link to="/manage/accounting/receivables">
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="payables">
|
||||
<Link to="/manage/accounting/payables">
|
||||
{t("menus.header.accounting-payables")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="payments">
|
||||
<Link to="/manage/accounting/payments">
|
||||
{t("menus.header.accounting-payments")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu title={t("menus.header.shop")}>
|
||||
<Menu.Item key="shop">
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="temporarydocs">
|
||||
<Link to="/manage/temporarydocs">
|
||||
{t("menus.header.temporarydocs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
{
|
||||
// <Menu.Item key="shop-templates">
|
||||
// <Link to="/manage/shop/templates">
|
||||
// {t("menus.header.shop_templates")}
|
||||
// </Link>
|
||||
// </Menu.Item>
|
||||
}
|
||||
<Menu.Item
|
||||
key="reportcenter"
|
||||
onClick={() => {
|
||||
setReportCenterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.reportcenter")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shop-vendors">
|
||||
<Link to="/manage/shop/vendors">
|
||||
{t("menus.header.shop_vendors")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shop-csi">
|
||||
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
<GlobalSearch />
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<div>
|
||||
{currentUser.photoURL ? (
|
||||
<Avatar
|
||||
src={currentUser.photoURL}
|
||||
style={{
|
||||
margin: "10px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
style={{
|
||||
backgroundColor: "#87d068",
|
||||
margin: "10px",
|
||||
}}
|
||||
icon={<UserOutlined />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Menu.Item danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shiftclock">
|
||||
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile">
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
<span>{t("menus.currentuser.languageselector")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item actiontype="lang-select" key="en-US">
|
||||
{t("general.languages.english")}
|
||||
</Menu.Item>
|
||||
<Menu.Item actiontype="lang-select" key="fr-CA">
|
||||
{t("general.languages.french")}
|
||||
</Menu.Item>
|
||||
<Menu.Item actiontype="lang-select" key="es-MX">
|
||||
{t("general.languages.spanish")}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</Link>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.header-shop-logo {
|
||||
background-size: cover;
|
||||
max-width: 100%;
|
||||
max-height: 3.5rem;
|
||||
}
|
||||
.header-main-menu {
|
||||
//width: 95vw;
|
||||
//float: left;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export default function HelpRescue() {
|
||||
>
|
||||
<span>
|
||||
Enter your six-digit code, then click the Start Download button
|
||||
below{" "}
|
||||
below
|
||||
</span>
|
||||
<input type="text" name="Code" />
|
||||
<br />
|
||||
|
||||
@@ -25,5 +25,5 @@ export default function JiraSupportComponent() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div>JIra</div>;
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Form, Input, InputNumber, Modal, Radio, Select } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
|
||||
|
||||
export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const { data: VendorAutoCompleteData } = useQuery(
|
||||
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR
|
||||
);
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
form.submit();
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
const handleFinish = (values) => {
|
||||
const { sendtype, ...restVals } = values;
|
||||
console.log(restVals);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("job_special").thirdpartypayer.key,
|
||||
variables: { id: jobId },
|
||||
context: restVals,
|
||||
},
|
||||
{},
|
||||
sendtype
|
||||
);
|
||||
};
|
||||
|
||||
const handleInsSelect = (value, option) => {
|
||||
form.setFieldsValue({
|
||||
addr1: option.obj.name,
|
||||
addr2: option.obj.street1,
|
||||
addr3: option.obj.street2,
|
||||
city: option.obj.city,
|
||||
state: option.obj.state,
|
||||
zip: option.obj.zip,
|
||||
vendorid: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVendorSelect = (vendorid, opt) => {
|
||||
const vendor = VendorAutoCompleteData.vendors.filter(
|
||||
(v) => v.id === vendorid
|
||||
)[0];
|
||||
if (vendor) {
|
||||
form.setFieldsValue({
|
||||
addr1: vendor.name,
|
||||
addr2: vendor.street1,
|
||||
addr3: vendor.street2,
|
||||
city: vendor.city,
|
||||
state: vendor.state,
|
||||
zip: vendor.zip,
|
||||
ins_co_id: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" onClick={showModal}>
|
||||
{t("printcenter.jobs.3rdpartypayer")}
|
||||
</Button>
|
||||
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item label={t("bills.fields.vendor")} name="vendorid">
|
||||
<VendorSearchSelect
|
||||
options={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
|
||||
onSelect={handleVendorSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_ins_co.name")}
|
||||
name="ins_co_id"
|
||||
>
|
||||
<Select onSelect={handleInsSelect}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} obj={s} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.addr1")}
|
||||
name="addr1"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.addr2")}
|
||||
name="addr2"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.addr3")}
|
||||
name="addr3"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.city")}
|
||||
name="city"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.state")}
|
||||
name="state"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.zip")}
|
||||
name="zip"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.attn")}
|
||||
name="attn"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.ponumber")}
|
||||
name="ponumber"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.ded_amt")}
|
||||
name="ded_amt"
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.depreciation")}
|
||||
name="depreciation"
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.custgst")}
|
||||
name="custgst"
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.other")}
|
||||
name="other"
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.sendtype")}
|
||||
name="sendtype"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
|
||||
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -17,30 +17,49 @@ export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
|
||||
|
||||
const totals = jobTotals;
|
||||
|
||||
let billTotals = Dinero({ amount: 0 });
|
||||
let billTotals = Dinero();
|
||||
let billCms = Dinero();
|
||||
let lbrAdjustments = Dinero();
|
||||
|
||||
bills.forEach((i) =>
|
||||
i.billlines.forEach((il) => {
|
||||
billTotals = billTotals.add(
|
||||
Dinero({
|
||||
amount: Math.round(
|
||||
(il.actual_cost || 0) * (i.is_credit_memo ? -1 : 1) * 100
|
||||
),
|
||||
}).multiply(il.quantity)
|
||||
);
|
||||
if (!i.is_credit_memo) {
|
||||
billTotals = billTotals.add(
|
||||
Dinero({
|
||||
amount: Math.round((il.actual_price || 0) * 100),
|
||||
}).multiply(il.quantity)
|
||||
);
|
||||
} else {
|
||||
billCms = billCms.add(
|
||||
Dinero({
|
||||
amount: Math.round((il.actual_price || 0) * -100),
|
||||
}).multiply(il.quantity)
|
||||
);
|
||||
}
|
||||
if (il.deductedfromlbr) {
|
||||
console.log(i, "Deducting from labor.");
|
||||
lbrAdjustments = lbrAdjustments.add(
|
||||
Dinero({
|
||||
amount: Math.round((il.actual_price || 0) * 100),
|
||||
}).multiply(il.quantity)
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const discrepancy = Dinero(totals.parts.parts.total).subtract(billTotals);
|
||||
const totalPartsSublet = Dinero(totals.parts.parts.total).add(
|
||||
Dinero(totals.parts.sublets.total)
|
||||
);
|
||||
const discrepancy = totalPartsSublet.subtract(billTotals);
|
||||
|
||||
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
|
||||
|
||||
const discrepWithCms = discrepWithLbrAdj.subtract(billCms);
|
||||
return (
|
||||
<div className="job-bills-totals-container">
|
||||
<Statistic
|
||||
title={t("jobs.labels.partstotal")}
|
||||
value={Dinero(totals.parts.parts.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.subletstotal")}
|
||||
value={Dinero(totals.parts.sublets.total).toFormat()}
|
||||
title={t("jobs.labels.rosaletotal")}
|
||||
value={totalPartsSublet.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.retailtotal")}
|
||||
@@ -53,6 +72,28 @@ export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
|
||||
}}
|
||||
value={discrepancy.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.dedfromlbr")}
|
||||
value={lbrAdjustments.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.discrepwithlbradj")}
|
||||
valueStyle={{
|
||||
color: discrepWithLbrAdj.getAmount === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrepWithLbrAdj.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.billcmtotal")}
|
||||
value={billCms.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.discrepwithcms")}
|
||||
valueStyle={{
|
||||
color: discrepWithCms.getAmount === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrepWithCms.toFormat()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Form, notification, Switch } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
@@ -30,6 +30,7 @@ export function JobChecklistForm({
|
||||
currentUser,
|
||||
type,
|
||||
job,
|
||||
readOnly = false,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [intakeJob] = useMutation(UPDATE_JOB);
|
||||
@@ -57,10 +58,18 @@ export function JobChecklistForm({
|
||||
...(type === "intake" && {
|
||||
scheduled_completion: values.scheduled_completion,
|
||||
}),
|
||||
|
||||
...(type === "deliver" && {
|
||||
actual_completion: values.actual_completion,
|
||||
}),
|
||||
[(type === "intake" && "intakechecklist") ||
|
||||
(type === "deliver" && "deliverchecklist")]: {
|
||||
...values,
|
||||
form: formItems.map((fi) => {
|
||||
return {
|
||||
...fi,
|
||||
value: values[fi.name],
|
||||
};
|
||||
}),
|
||||
completed_by: currentUser.email,
|
||||
completed_at: new Date(),
|
||||
},
|
||||
@@ -112,11 +121,21 @@ export function JobChecklistForm({
|
||||
scheduled_completion: job && job.scheduled_completion,
|
||||
scheduled_delivery: job && job.scheduled_delivery,
|
||||
}),
|
||||
...(type === "deliver" && {
|
||||
removeFromProduction: true,
|
||||
actual_completion: job && job.actual_completion,
|
||||
}),
|
||||
...formItems
|
||||
.filter((fi) => fi.value)
|
||||
.reduce((acc, fi) => {
|
||||
acc[fi.name] = fi.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
}}
|
||||
>
|
||||
{t("checklist.labels.checklist")}
|
||||
|
||||
<ConfigFormComponents componentList={formItems} />
|
||||
<ConfigFormComponents componentList={formItems} readOnly={readOnly} />
|
||||
|
||||
{type === "intake" && (
|
||||
<div>
|
||||
@@ -124,12 +143,14 @@ export function JobChecklistForm({
|
||||
name="addToProduction"
|
||||
valuePropName="checked"
|
||||
label={t("checklist.labels.addtoproduction")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduled_completion"
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
disabled={readOnly}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -142,6 +163,7 @@ export function JobChecklistForm({
|
||||
<Form.Item
|
||||
name="scheduled_delivery"
|
||||
label={t("jobs.fields.scheduled_delivery")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
@@ -150,8 +172,9 @@ export function JobChecklistForm({
|
||||
{type === "deliver" && (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="actualCompletion"
|
||||
name="actual_completion"
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
disabled={readOnly}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -165,15 +188,17 @@ export function JobChecklistForm({
|
||||
name="removeFromProduction"
|
||||
valuePropName="checked"
|
||||
label={t("checklist.labels.removefromproduction")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Switch />
|
||||
<Switch defaultChecked={true} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button loading={loading} htmlType="submit">
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
{!readOnly && (
|
||||
<Button loading={loading} htmlType="submit">
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobChecklistTemplateItem({
|
||||
templateKey,
|
||||
renderTemplate,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{t(`printcenter.jobs.${templateKey}`)}
|
||||
<PrinterFilled onClick={() => renderTemplate(templateKey)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +1,77 @@
|
||||
import React from "react";
|
||||
import JobIntakeTemplateItem from "../job-checklist-template-item/job-checklist-template-item.component";
|
||||
import { useParams } from "react-router-dom";
|
||||
import RenderTemplate, {
|
||||
displayTemplateInWindow,
|
||||
} from "../../../../utils/RenderTemplate";
|
||||
import { Button } from "antd";
|
||||
import { selectBodyshop } from "../../../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import { Button, List } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||
import {
|
||||
GenerateDocument,
|
||||
GenerateDocuments,
|
||||
} from "../../../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../../../utils/TemplateConstants";
|
||||
const TemplateListGenerated = TemplateList();
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobIntakeTemplateList({ bodyshop, templates }) {
|
||||
export default function JobIntakeTemplateList({ templates }) {
|
||||
const { jobId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderTemplate = async (templateKey) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("job_checklist_template_render");
|
||||
|
||||
const html = await RenderTemplate(
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: templateKey,
|
||||
variables: { id: jobId },
|
||||
},
|
||||
bodyshop
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
displayTemplateInWindow(html);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const renderAllTemplates = () => {
|
||||
logImEXEvent("job_checklist_render_all_templates");
|
||||
|
||||
templates.forEach((template) => renderTemplate(template));
|
||||
const renderAllTemplates = async () => {
|
||||
logImEXEvent("checklist_render_all_templates");
|
||||
setLoading(true);
|
||||
console.log("templates :>> ", templates);
|
||||
await GenerateDocuments(
|
||||
templates.map((key) => {
|
||||
return { name: key, variables: { id: jobId } };
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{t("intake.labels.printpack")}
|
||||
<Button onClick={renderAllTemplates}>
|
||||
<Button onClick={renderAllTemplates} loading={loading}>
|
||||
{t("checklist.actions.printall")}
|
||||
</Button>
|
||||
{templates.map((template) => (
|
||||
<JobIntakeTemplateItem
|
||||
key={template}
|
||||
templateKey={template}
|
||||
renderTemplate={renderTemplate}
|
||||
/>
|
||||
))}
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={templates}
|
||||
renderItem={(template) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
loading={loading}
|
||||
onClick={() => renderTemplate(template)}
|
||||
>
|
||||
<PrinterFilled />
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
TemplateListGenerated[template] &&
|
||||
TemplateListGenerated[template].title
|
||||
}
|
||||
// description={renderTemplateList(template.templates)}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobIntakeTemplateList);
|
||||
|
||||
@@ -6,11 +6,11 @@ export default function JobIntakeComponent({ checklistConfig, type, job }) {
|
||||
const { form, templates } = checklistConfig;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Row gutter={[48, 48]}>
|
||||
<Col span={6}>
|
||||
<JobChecklistTemplateList templates={templates} type={type} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col span={18}>
|
||||
<JobChecklistForm formItems={form} type={type} job={job} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Typography } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
|
||||
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
|
||||
|
||||
import JobCostingPie from "./job-costing-modal.pie.component";
|
||||
import _ from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
@@ -15,42 +18,49 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
||||
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
const jobLineTotalsByProfitCenter = job.joblines.reduce(
|
||||
(acc, val) => {
|
||||
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
|
||||
|
||||
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
||||
const laborAmount = Dinero({
|
||||
amount: Math.round((job[rateName] || 0) * 100),
|
||||
}).multiply(val.mod_lb_hrs || 0);
|
||||
if (!!!acc.labor[laborProfitCenter])
|
||||
acc.labor[laborProfitCenter] = Dinero();
|
||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
||||
laborAmount
|
||||
);
|
||||
|
||||
const partsProfitCenter = defaultProfits[val.part_type] || "?";
|
||||
if (!!!partsProfitCenter)
|
||||
console.log(
|
||||
"Unknown cost/profit center mapping for parts.",
|
||||
val.part_type
|
||||
);
|
||||
const partsAmount = Dinero({
|
||||
amount: Math.round((val.act_price || 0) * 100),
|
||||
}).multiply(val.part_qty || 1);
|
||||
if (!!!acc.parts[partsProfitCenter])
|
||||
acc.parts[partsProfitCenter] = Dinero();
|
||||
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
|
||||
partsAmount
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ parts: {}, labor: {} }
|
||||
const allProfitCenters = _.union(
|
||||
bodyshop.md_responsibility_centers.profits.map((p) => p.name),
|
||||
bodyshop.md_responsibility_centers.costs.map((p) => p.name)
|
||||
);
|
||||
|
||||
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
||||
const { t } = useTranslation();
|
||||
const jobLineTotalsByProfitCenter =
|
||||
job &&
|
||||
job.joblines.reduce(
|
||||
(acc, val) => {
|
||||
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
|
||||
|
||||
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
||||
const laborAmount = Dinero({
|
||||
amount: Math.round((job[rateName] || 0) * 100),
|
||||
}).multiply(val.mod_lb_hrs || 0);
|
||||
if (!!!acc.labor[laborProfitCenter])
|
||||
acc.labor[laborProfitCenter] = Dinero();
|
||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
||||
laborAmount
|
||||
);
|
||||
|
||||
const partsProfitCenter = defaultProfits[val.part_type] || "?";
|
||||
if (!!!partsProfitCenter)
|
||||
console.log(
|
||||
"Unknown cost/profit center mapping for parts.",
|
||||
val.part_type
|
||||
);
|
||||
const partsAmount = Dinero({
|
||||
amount: Math.round((val.act_price || 0) * 100),
|
||||
}).multiply(val.part_qty || 1);
|
||||
if (!!!acc.parts[partsProfitCenter])
|
||||
acc.parts[partsProfitCenter] = Dinero();
|
||||
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
|
||||
partsAmount
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ parts: {}, labor: {} }
|
||||
);
|
||||
|
||||
const billTotalsByProfitCenter = job.bills.reduce((bill_acc, bill_val) => {
|
||||
//At the invoice level.
|
||||
bill_val.billlines.map((line_val) => {
|
||||
@@ -66,6 +76,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
.multiply(line_val.quantity)
|
||||
.multiply(bill_val.is_credit_memo ? -1 : 1)
|
||||
);
|
||||
|
||||
return null;
|
||||
});
|
||||
return bill_acc;
|
||||
@@ -82,7 +93,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
].add(
|
||||
Dinero({
|
||||
amount: Math.round((ticket_val.rate || 0) * 100),
|
||||
}).multiply(ticket_val.actualhrs || 0)
|
||||
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
|
||||
);
|
||||
|
||||
return ticket_acc;
|
||||
@@ -102,8 +113,8 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
gppercentFormatted: null,
|
||||
};
|
||||
|
||||
const costCenterData = Object.keys(defaultProfits).map((key, idx) => {
|
||||
const ccVal = defaultProfits[key];
|
||||
const costCenterData = allProfitCenters.map((key, idx) => {
|
||||
const ccVal = key; // defaultProfits[key];
|
||||
const sale_labor =
|
||||
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
|
||||
const sale_parts =
|
||||
@@ -113,11 +124,11 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||
|
||||
const cost = (billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })).add(
|
||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
||||
);
|
||||
const costs = (
|
||||
billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
||||
).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
|
||||
const totalSales = sale_labor.add(sale_parts);
|
||||
const gpdollars = totalSales.subtract(cost);
|
||||
const gpdollars = totalSales.subtract(costs);
|
||||
const gppercent = (
|
||||
(gpdollars.getAmount() / totalSales.getAmount()) *
|
||||
100
|
||||
@@ -137,16 +148,19 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
.add(sale_parts);
|
||||
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
|
||||
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
|
||||
summaryData.totalCost = summaryData.totalCost.add(cost);
|
||||
summaryData.totalCost = summaryData.totalCost.add(costs);
|
||||
|
||||
return {
|
||||
id: idx,
|
||||
cost_center: ccVal,
|
||||
sale_labor: sale_labor && sale_labor.toFormat(),
|
||||
sale_parts: sale_parts && sale_parts.toFormat(),
|
||||
sales: sale_labor.add(sale_parts).toFormat(),
|
||||
sales_dinero: sale_labor.add(sale_parts),
|
||||
cost_parts: cost_parts && cost_parts.toFormat(),
|
||||
cost_labor: cost_labor && cost_labor.toFormat(),
|
||||
cost: cost && cost.toFormat(),
|
||||
costs: cost_parts.add(cost_labor).toFormat(),
|
||||
costs_dinero: cost_parts.add(cost_labor),
|
||||
gpdollars: gpdollars.toFormat(),
|
||||
gppercent: gppercentFormatted,
|
||||
};
|
||||
@@ -170,7 +184,23 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
return (
|
||||
<div>
|
||||
<JobCostingStatistics job={job} summaryData={summaryData} />
|
||||
<JobCostingPartsTable job={job} data={costCenterData} />
|
||||
<JobCostingPartsTable
|
||||
job={job}
|
||||
data={costCenterData}
|
||||
summaryData={summaryData}
|
||||
/>
|
||||
<div className="imex-flex-row">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography.Title level={4}>
|
||||
{t("jobs.labels.sales")}
|
||||
</Typography.Title>
|
||||
<JobCostingPie type="sales" costCenterData={costCenterData} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography.Title level={4}>{t("jobs.labels.cost")}</Typography.Title>
|
||||
<JobCostingPie type="cost" costCenterData={costCenterData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Modal } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -37,10 +37,11 @@ export function JobCostingModalContainer({
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("jobs.labels.jobcosting")}
|
||||
onOk={() => toggleModalVisible()}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
width="90%"
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{loading ? (
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
|
||||
|
||||
export default function JobCostingPieComponent({
|
||||
type = "sales",
|
||||
costCenterData,
|
||||
}) {
|
||||
const Calculatedata = useCallback(
|
||||
(data) => {
|
||||
if (data && data.length > 0) {
|
||||
return data.reduce((acc, i) => {
|
||||
const value =
|
||||
type === "sales"
|
||||
? i.sales_dinero.getAmount()
|
||||
: i.costs_dinero.getAmount();
|
||||
|
||||
if (value > 0) {
|
||||
acc.push({
|
||||
name: i.cost_center,
|
||||
color: "#" + Math.floor(Math.random() * 16777215).toString(16),
|
||||
|
||||
label: `${i.cost_center} - ${
|
||||
type === "sales"
|
||||
? i.sales_dinero.toFormat()
|
||||
: i.costs_dinero.toFormat()
|
||||
}`,
|
||||
value:
|
||||
type === "sales"
|
||||
? i.sales_dinero.getAmount()
|
||||
: i.costs_dinero.getAmount(),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[type]
|
||||
);
|
||||
|
||||
const memoizedData = useMemo(() => Calculatedata(costCenterData), [
|
||||
costCenterData,
|
||||
Calculatedata,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={175}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={memoizedData}
|
||||
innerRadius={40}
|
||||
outerRadius={50}
|
||||
fill="#8884d8"
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
label={(entry) => entry.label}
|
||||
labelLine
|
||||
>
|
||||
{memoizedData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Table } from "antd";
|
||||
import { Input, Table, Typography } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function JobCostingPartsTable({ job, data }) {
|
||||
export default function JobCostingPartsTable({ job, data, summaryData }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
@@ -24,37 +25,23 @@ export default function JobCostingPartsTable({ job, data }) {
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.sale_labor"),
|
||||
dataIndex: "sale_labor",
|
||||
key: "sale_labor",
|
||||
sorter: (a, b) => alphaSort(a.sale_labor, b.sale_labor),
|
||||
title: t("jobs.labels.sales"),
|
||||
dataIndex: "sales",
|
||||
key: "sales",
|
||||
sorter: (a, b) => alphaSort(a.sales, b.sales),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "sale_labor" && state.sortedInfo.order,
|
||||
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.sale_parts"),
|
||||
dataIndex: "sale_parts",
|
||||
key: "sale_parts",
|
||||
sorter: (a, b) => alphaSort(a.sale_parts, b.sale_parts),
|
||||
title: t("jobs.labels.costs"),
|
||||
dataIndex: "costs",
|
||||
key: "costs",
|
||||
sorter: (a, b) => a.costs - b.costs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "sale_parts" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.cost_labor"),
|
||||
dataIndex: "cost_labor",
|
||||
key: "cost_labor",
|
||||
sorter: (a, b) => a.cost_labor - b.cost_labor,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_labor" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.cost_parts"),
|
||||
dataIndex: "cost_parts",
|
||||
key: "cost_parts",
|
||||
sorter: (a, b) => a.cost_parts - b.cost_parts,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_parts" && state.sortedInfo.order,
|
||||
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.gpdollars"),
|
||||
dataIndex: "gpdollars",
|
||||
@@ -73,16 +60,61 @@ export default function JobCostingPartsTable({ job, data }) {
|
||||
},
|
||||
];
|
||||
|
||||
const filteredData =
|
||||
searchText === ""
|
||||
? data
|
||||
: data.filter((d) =>
|
||||
(d.cost_center || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
size="small"
|
||||
title={() => {
|
||||
return (
|
||||
<div className="imex-table-header">
|
||||
<div className="imex-table-header__search">
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
scroll={{ x: "50%", y: "40rem" }}
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
dataSource={filteredData}
|
||||
summary={() => (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.totals")}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{summaryData.totalSales.toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{summaryData.totalCost.toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{summaryData.gpdollars.toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell></Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user