@@ -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/node_modules
|
||||||
admin/.pnp
|
admin/.pnp
|
||||||
admin.pnp.js
|
admin.pnp.js
|
||||||
|
jsreport/node_modules
|
||||||
|
jsreport/auth-server/node_modules
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
client/coverage
|
client/coverage
|
||||||
@@ -37,6 +39,7 @@ client/yarn-error.log*
|
|||||||
admin/npm-debug.log*
|
admin/npm-debug.log*
|
||||||
admin/yarn-debug.log*
|
admin/yarn-debug.log*
|
||||||
admin/yarn-error.log*
|
admin/yarn-error.log*
|
||||||
|
client/.eslintcache
|
||||||
|
|
||||||
#Firebase Ignore
|
#Firebase Ignore
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ To Start Hasura CLI:
|
|||||||
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
||||||
|
|
||||||
Migrating to Staging:
|
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 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
|
Finding deadfiles - run from client directory
|
||||||
npx deadfile ./src/index.js --exclude build templates
|
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
|
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",
|
"apollo-link-logger": "^1.2.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"firebase": "^7.21.0",
|
"firebase": "^7.21.0",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.4.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"ra-data-hasura-graphql": "^0.1.12",
|
"ra-data-hasura-graphql": "^0.1.12",
|
||||||
"react": "^16.13.1",
|
"react": "^17.0.1",
|
||||||
"react-admin": "^3.8.5",
|
"react-admin": "^3.8.5",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^3.11.0",
|
||||||
"react-scripts": "3.4.3"
|
"react-scripts": "4.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "set PORT=3001 && react-scripts start",
|
"start": "set PORT=3001 && react-scripts start",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const JobsCreate = (props) => (
|
|||||||
<Create {...props}>
|
<Create {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="ro_number" />
|
<TextInput source="ro_number" />
|
||||||
<TextInput source="est_number" />
|
|
||||||
<TextInput source="ownr_fn" />
|
<TextInput source="ownr_fn" />
|
||||||
<TextInput source="ownr_ln" />
|
<TextInput source="ownr_ln" />
|
||||||
<TextInput source="converted" />
|
<TextInput source="converted" />
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ const JobsEdit = (props) => (
|
|||||||
<TextInput fullWidth source="date_open" />
|
<TextInput fullWidth source="date_open" />
|
||||||
<TextInput fullWidth source="date_scheduled" />
|
<TextInput fullWidth source="date_scheduled" />
|
||||||
<TextInput fullWidth source="date_invoiced" />
|
<TextInput fullWidth source="date_invoiced" />
|
||||||
<TextInput fullWidth source="date_closed" />
|
|
||||||
<TextInput fullWidth source="date_exported" />
|
<TextInput fullWidth source="date_exported" />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
<FormTab label="Insurance info">
|
<FormTab label="Insurance info">
|
||||||
@@ -283,7 +282,6 @@ const JobsEdit = (props) => (
|
|||||||
<FormTab label="Other">
|
<FormTab label="Other">
|
||||||
<TextInput fullWidth source="area_of_damage" />
|
<TextInput fullWidth source="area_of_damage" />
|
||||||
<TextInput fullWidth source="loss_cat" />
|
<TextInput fullWidth source="loss_cat" />
|
||||||
<TextInput fullWidth source="est_number" />
|
|
||||||
<TextInput fullWidth source="special_coverage_policy" />
|
<TextInput fullWidth source="special_coverage_policy" />
|
||||||
<TextInput fullWidth source="csr" />
|
<TextInput fullWidth source="csr" />
|
||||||
<TextInput fullWidth source="po_number" />
|
<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 React from "react";
|
||||||
import {
|
import {
|
||||||
Datagrid,
|
Datagrid,
|
||||||
Filter,
|
Filter,
|
||||||
List,
|
List,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
TextField,
|
|
||||||
SelectInput,
|
SelectInput,
|
||||||
TextInput,
|
TextField,
|
||||||
|
TextInput
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
|
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
|
||||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
|
||||||
|
|
||||||
const JobsList = (props) => (
|
const JobsList = (props) => (
|
||||||
<List filters={<JobsFilter />} {...props}>
|
<List filters={<JobsFilter />} {...props}>
|
||||||
@@ -20,7 +20,7 @@ const JobsList = (props) => (
|
|||||||
<TextField source="shopname" />
|
<TextField source="shopname" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<TextField source="ro_number" />
|
<TextField source="ro_number" />
|
||||||
<TextField source="est_number" />
|
|
||||||
<TextField source="ownr_fn" />
|
<TextField source="ownr_fn" />
|
||||||
<TextField source="ownr_ln" />
|
<TextField source="ownr_ln" />
|
||||||
<TextField source="ownr_co_nm" />
|
<TextField source="ownr_co_nm" />
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ const JobsShow = (props) => (
|
|||||||
<TextField source="date_open" />
|
<TextField source="date_open" />
|
||||||
<TextField source="date_scheduled" />
|
<TextField source="date_scheduled" />
|
||||||
<TextField source="date_invoiced" />
|
<TextField source="date_invoiced" />
|
||||||
<TextField source="date_closed" />
|
|
||||||
<TextField source="date_exported" />
|
<TextField source="date_exported" />
|
||||||
<TextField source="clm_total" />
|
<TextField source="clm_total" />
|
||||||
<TextField source="owner_owing" />
|
<TextField source="owner_owing" />
|
||||||
@@ -221,7 +220,7 @@ const JobsShow = (props) => (
|
|||||||
<TextField source="ownr_ea" />
|
<TextField source="ownr_ea" />
|
||||||
<TextField source="area_of_damage" />
|
<TextField source="area_of_damage" />
|
||||||
<TextField source="loss_cat" />
|
<TextField source="loss_cat" />
|
||||||
<TextField source="est_number" />
|
|
||||||
<TextField source="special_coverage_policy" />
|
<TextField source="special_coverage_policy" />
|
||||||
<TextField source="csr" />
|
<TextField source="csr" />
|
||||||
<TextField source="po_number" />
|
<TextField source="po_number" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import gql from "graphql-tag";
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
export const QUERY_ALL_SHOPS = gql`
|
export const QUERY_ALL_SHOPS = gql`
|
||||||
query QUERY_ALL_SHOPS {
|
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",
|
"name": "bodyshop",
|
||||||
"version": "0.1.0001",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:5000",
|
"proxy": "http://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lourenci/react-kanban": "^2.0.0",
|
"@apollo/client": "^3.3.11",
|
||||||
"@stripe/react-stripe-js": "^1.1.2",
|
"@fingerprintjs/fingerprintjs": "^3.0.6",
|
||||||
"@stripe/stripe-js": "^1.9.0",
|
"@lourenci/react-kanban": "^2.1.0",
|
||||||
"@tanem/react-nprogress": "^3.0.46",
|
"@sentry/react": "^6.2.0",
|
||||||
"@tinymce/tinymce-react": "^3.7.0",
|
"@sentry/tracing": "^6.2.0",
|
||||||
"antd": "^4.6.6",
|
"@stripe/react-stripe-js": "^1.2.2",
|
||||||
"apollo-boost": "^0.4.9",
|
"@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",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"axios": "^0.20.0",
|
"axios": "^0.21.1",
|
||||||
"codemirror": "^5.58.1",
|
|
||||||
"codemirror-graphql": "^0.12.2",
|
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.8.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"fingerprintjs2": "^2.1.2",
|
"firebase": "^8.2.9",
|
||||||
"firebase": "^7.22.1",
|
"graphql": "^15.5.0",
|
||||||
"graphql": "^15.3.0",
|
"i18next": "^19.8.9",
|
||||||
"i18next": "^19.8.2",
|
|
||||||
"i18next-browser-languagedetector": "^6.0.1",
|
"i18next-browser-languagedetector": "^6.0.1",
|
||||||
"inline-css": "^2.6.3",
|
"jsoneditor": "^9.1.10",
|
||||||
"jsoneditor": "^9.1.1",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"jsoneditor-react": "^3.0.1",
|
"libphonenumber-js": "^1.9.11",
|
||||||
"logrocket": "^1.0.13",
|
"logrocket": "^1.0.13",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"phone": "^2.4.16",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.13.5",
|
"query-string": "^6.14.0",
|
||||||
"react": "^16.13.1",
|
"react": "^17.0.1",
|
||||||
"react-apollo": "^3.1.5",
|
"react-big-calendar": "^0.30.0",
|
||||||
"react-big-calendar": "^0.28.0",
|
"react-color": "^2.19.3",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-color": "^2.18.1",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-dom": "^16.13.1",
|
|
||||||
"react-drag-listview": "^0.1.7",
|
|
||||||
"react-email-editor": "^1.1.1",
|
|
||||||
"react-ga": "^3.1.2",
|
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-i18next": "^11.7.3",
|
"react-i18next": "^11.8.7",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-moment": "^1.0.0",
|
"react-number-format": "^4.4.4",
|
||||||
"react-number-format": "^4.4.1",
|
"react-phone-input-2": "^2.13.9",
|
||||||
"react-redux": "^7.2.1",
|
"react-redux": "^7.2.2",
|
||||||
"react-resizable": "^1.11.0",
|
"react-resizable": "^1.11.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "3.4.3",
|
"react-scripts": "^4.0.3",
|
||||||
"react-trello": "^2.2.8",
|
"react-virtualized": "^9.22.3",
|
||||||
"react-virtualized": "^9.22.2",
|
"recharts": "^2.0.7",
|
||||||
"recharts": "^1.8.5",
|
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
@@ -64,6 +59,7 @@
|
|||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
"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",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
||||||
@@ -84,10 +80,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apollo/react-testing": "^4.0.0",
|
|
||||||
"enzyme": "^3.11.0",
|
|
||||||
"enzyme-adapter-react-16": "^1.15.5",
|
|
||||||
"redux-logger": "^3.0.6",
|
"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" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#002366" />
|
<meta name="theme-color" content="#002366" />
|
||||||
<meta
|
<meta name="description" content="ImEX Online" />
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||||
<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 { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||||
//Component Imports
|
//Component Imports
|
||||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
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 TechPageContainer from "../pages/tech/tech.page.container";
|
||||||
import { checkUserSession } from "../redux/user/user.actions";
|
import { checkUserSession } from "../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../redux/user/user.selectors";
|
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||||
@@ -62,6 +63,9 @@ export function App({ checkUserSession, currentUser }) {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Route exact path="/about" component={AboutPage} />
|
||||||
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
|||||||
@@ -49,25 +49,77 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
// ::-webkit-scrollbar-track {
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 0.2rem;
|
// border-radius: 0.2rem;
|
||||||
background-color: #f5f5f5;
|
// background-color: #f5f5f5;
|
||||||
}
|
// }
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
// ::-webkit-scrollbar {
|
||||||
width: 0.25rem;
|
// width: 0.25rem;
|
||||||
max-height: 0.25rem;
|
// max-height: 0.25rem;
|
||||||
background-color: #f5f5f5;
|
// background-color: #f5f5f5;
|
||||||
}
|
// }
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
// ::-webkit-scrollbar-thumb {
|
||||||
border-radius: 0.2rem;
|
// border-radius: 0.2rem;
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
background-color: #188fff;
|
// background-color: #188fff;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.ant-table-cell {
|
.ant-table-cell {
|
||||||
// background-color: red;
|
// background-color: red;
|
||||||
//padding: 0.2rem !important;
|
//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,
|
replyTo: bodyshop.email,
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
name: TemplateList().parts_order_confirmation.key,
|
name: TemplateList().parts_order.key,
|
||||||
variables: {
|
variables: {
|
||||||
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,19 +150,20 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
title={() => {
|
title={() => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="imex-table-header">
|
||||||
|
<PayableExportAll
|
||||||
|
billids={selectedBills}
|
||||||
|
disabled={transInProgress || selectedBills.length === 0}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
completedCallback={setSelectedBills}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
className="imex-table-header__search"
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<PayableExportAll
|
|
||||||
billIds={selectedBills}
|
|
||||||
disabled={transInProgress || selectedBills.length === 0}
|
|
||||||
loadingCallback={setTransInProgress}
|
|
||||||
completedCallback={setSelectedBills}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Input, Table } from "antd";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
|
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
export default function AccountingPayablesTableComponent({
|
export default function AccountingPayablesTableComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -38,17 +38,21 @@ export default function AccountingPayablesTableComponent({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.est_number"),
|
title: t("payments.fields.date"),
|
||||||
dataIndex: "est_number",
|
dataIndex: "date",
|
||||||
key: "est_number",
|
key: "date",
|
||||||
sorter: (a, b) => a.job.est_number - b.job.est_number,
|
sorter: (a, b) => alphaSort(a.date, b.date),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>
|
},
|
||||||
{record.job.est_number}
|
{
|
||||||
</Link>
|
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"),
|
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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import { JobsExportAllButton } from "../jobs-export-all-button/jobs-export-all-button.component";
|
import { JobsExportAllButton } from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
|
||||||
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
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>
|
<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"),
|
title: t("jobs.fields.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
@@ -156,10 +146,6 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
|||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(v.est_number || "")
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(v.ownr_fn || "")
|
(v.ownr_fn || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Alert } from "antd";
|
import { Alert } from "antd";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
export default function AlertComponent(props) {
|
export default function AlertComponent(props) {
|
||||||
if (props.type === "error") logImEXEvent("alert_render", { ...props });
|
if (props.type === "error") logImEXEvent("alert_render", { ...props });
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import Alert from "./alert.component";
|
import Alert from "./alert.component";
|
||||||
import { MockedProvider } from "@apollo/react-testing";
|
|
||||||
import { shallow, mount } from "enzyme";
|
|
||||||
|
|
||||||
const div = document.createElement("div");
|
|
||||||
|
|
||||||
describe("Alert component", () => {
|
describe("Alert component", () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "Test error message."
|
message: "Test error message.",
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapper = shallow(<Alert {...mockProps} />);
|
wrapper = shallow(<Alert {...mockProps} />);
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { mount, shallow } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
|
||||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||||
import { Select } from "antd";
|
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||||
const div = document.createElement("div");
|
|
||||||
|
|
||||||
describe("AllocationsAssignmentComponent component", () => {
|
describe("AllocationsAssignmentComponent component", () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
||||||
import { useMutation } from "@apollo/react-hooks";
|
import { useMutation } from "@apollo/client";
|
||||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
@@ -8,29 +8,29 @@ import { notification } from "antd";
|
|||||||
export default function AllocationsAssignmentContainer({
|
export default function AllocationsAssignmentContainer({
|
||||||
jobLineId,
|
jobLineId,
|
||||||
hours,
|
hours,
|
||||||
refetch
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const visibilityState = useState(false);
|
const visibilityState = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [assignment, setAssignment] = useState({
|
const [assignment, setAssignment] = useState({
|
||||||
joblineid: jobLineId,
|
joblineid: jobLineId,
|
||||||
hours: parseFloat(hours),
|
hours: parseFloat(hours),
|
||||||
employeeid: null
|
employeeid: null,
|
||||||
});
|
});
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||||
|
|
||||||
const handleAssignment = () => {
|
const handleAssignment = () => {
|
||||||
insertAllocation({ variables: { alloc: { ...assignment } } })
|
insertAllocation({ variables: { alloc: { ...assignment } } })
|
||||||
.then(r => {
|
.then((r) => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("allocations.successes.save")
|
message: t("allocations.successes.save"),
|
||||||
});
|
});
|
||||||
visibilityState[1](false);
|
visibilityState[1](false);
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
notification["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 React, { useState } from "react";
|
||||||
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
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 { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|
||||||
export default function AllocationsBulkAssignmentContainer({
|
export default function AllocationsBulkAssignmentContainer({
|
||||||
jobLines,
|
jobLines,
|
||||||
refetch
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const visibilityState = useState(false);
|
const visibilityState = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [assignment, setAssignment] = useState({
|
const [assignment, setAssignment] = useState({
|
||||||
employeeid: null
|
employeeid: null,
|
||||||
});
|
});
|
||||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ export default function AllocationsBulkAssignmentContainer({
|
|||||||
acc.push({
|
acc.push({
|
||||||
joblineid: value.id,
|
joblineid: value.id,
|
||||||
hours: parseFloat(value.mod_lb_hrs) || 0,
|
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||||
employeeid: assignment.employeeid
|
employeeid: assignment.employeeid,
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
insertAllocation({ variables: { alloc: allocs } }).then(r => {
|
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("employees.successes.save")
|
message: t("employees.successes.save"),
|
||||||
});
|
});
|
||||||
visibilityState[1](false);
|
visibilityState[1](false);
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMutation } from "@apollo/react-hooks";
|
import { useMutation } from "@apollo/client";
|
||||||
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
||||||
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import AuditTrailListComponent from "./audit-trail-list.component";
|
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/client";
|
||||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
@@ -15,7 +15,7 @@ export default function AuditTrailListContainer({ recordId }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<AlertComponent type='error' message={error.message} />
|
<AlertComponent type="error" message={error.message} />
|
||||||
) : (
|
) : (
|
||||||
<AuditTrailListComponent
|
<AuditTrailListComponent
|
||||||
loading={loading}
|
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 { Button, Form } from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import queryString from "query-string";
|
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 { Button, Form, Modal, notification } from "antd";
|
||||||
|
import _ from "lodash";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
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 { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
import {
|
import {
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -34,42 +39,123 @@ function BillEnterModalContainer({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const client = useApolloClient();
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { upload, location, ...remainingValues } = values;
|
const { upload, location, ...remainingValues } = values;
|
||||||
insertBill({
|
|
||||||
|
let adjustmentsToInsert = {};
|
||||||
|
|
||||||
|
const r1 = await insertBill({
|
||||||
variables: {
|
variables: {
|
||||||
bill: [
|
bill: [
|
||||||
Object.assign({}, remainingValues, {
|
{
|
||||||
|
...remainingValues,
|
||||||
billlines: {
|
billlines: {
|
||||||
data:
|
data:
|
||||||
remainingValues.billlines &&
|
remainingValues.billlines &&
|
||||||
remainingValues.billlines.map((i) => {
|
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 {
|
return {
|
||||||
...i,
|
...restI,
|
||||||
|
deductedfromlbr: deductfromlabor,
|
||||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then((r) => {
|
console.log("adjustmentsToInsert", adjustmentsToInsert);
|
||||||
const billId = r.data.insert_bills.returning[0].id;
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
|
if (adjKeys.length > 0) {
|
||||||
updateJobLines({
|
//Query the adjustments, merge, and update them.
|
||||||
|
const existingAdjustments = await client.query({
|
||||||
|
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
variables: {
|
variables: {
|
||||||
ids: remainingValues.billlines
|
id: values.jobid,
|
||||||
.filter((il) => il.joblineid !== "noline")
|
|
||||||
.map((li) => li.joblineid),
|
|
||||||
status: bodyshop.md_order_statuses.default_received || "Received*",
|
|
||||||
location: location,
|
|
||||||
},
|
},
|
||||||
}).then((joblineresult) => {
|
});
|
||||||
|
|
||||||
|
const newAdjustments = _.cloneDeep(
|
||||||
|
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||||
|
);
|
||||||
|
|
||||||
|
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("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) {
|
if (upload && upload.length > 0) {
|
||||||
//insert Each of the documents?
|
//insert Each of the documents?
|
||||||
@@ -101,17 +187,6 @@ function BillEnterModalContainer({
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}
|
}
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setLoading(false);
|
|
||||||
setEnterAgain(false);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("bills.errors.creating", {
|
|
||||||
message: JSON.stringify(error),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@@ -183,7 +258,10 @@ function BillEnterModalContainer({
|
|||||||
0,
|
0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BillFormContainer form={form} />
|
<BillFormContainer
|
||||||
|
form={form}
|
||||||
|
disableInvNumber={billEnterModal.context.disableInvNumber}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useApolloClient } from "react-apollo";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -18,12 +18,14 @@ import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.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 CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import BillFormLines from "./bill-form.lines.component";
|
import BillFormLines from "./bill-form.lines.component";
|
||||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -38,6 +40,7 @@ export function BillFormComponent({
|
|||||||
responsibilityCenters,
|
responsibilityCenters,
|
||||||
loadLines,
|
loadLines,
|
||||||
billEdit,
|
billEdit,
|
||||||
|
disableInvNumber,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -47,7 +50,6 @@ export function BillFormComponent({
|
|||||||
setDiscount(opt.discount);
|
setDiscount(opt.discount);
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO: Test this further. Required to set discount when viewing an invoice.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
||||||
const vendorId = form.getFieldValue("vendorid");
|
const vendorId = form.getFieldValue("vendorid");
|
||||||
@@ -65,7 +67,15 @@ export function BillFormComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow>
|
<FormFieldsChanged form={form} />
|
||||||
|
<Form.Item
|
||||||
|
style={{ display: "none" }}
|
||||||
|
name="isinhouse"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="jobid"
|
name="jobid"
|
||||||
label={t("bills.fields.ro_number")}
|
label={t("bills.fields.ro_number")}
|
||||||
@@ -78,6 +88,8 @@ export function BillFormComponent({
|
|||||||
>
|
>
|
||||||
<JobSearchSelect
|
<JobSearchSelect
|
||||||
disabled={billEdit || disabled}
|
disabled={billEdit || disabled}
|
||||||
|
convertedOnly
|
||||||
|
// notExported={false}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (form.getFieldValue("jobid") !== null) {
|
if (form.getFieldValue("jobid") !== null) {
|
||||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||||
@@ -103,7 +115,6 @@ export function BillFormComponent({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.invoice_number")}
|
label={t("bills.fields.invoice_number")}
|
||||||
@@ -118,7 +129,7 @@ export function BillFormComponent({
|
|||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
async validator(rule, value) {
|
async validator(rule, value) {
|
||||||
const vendorid = getFieldValue("vendorid");
|
const vendorid = getFieldValue("vendorid");
|
||||||
if (vendorid) {
|
if (vendorid && value) {
|
||||||
const response = await client.query({
|
const response = await client.query({
|
||||||
query: CHECK_BILL_INVOICE_NUMBER,
|
query: CHECK_BILL_INVOICE_NUMBER,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -129,6 +140,12 @@ export function BillFormComponent({
|
|||||||
|
|
||||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||||
return Promise.resolve();
|
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(
|
return Promise.reject(
|
||||||
t("bills.validation.unique_invoice_number")
|
t("bills.validation.unique_invoice_number")
|
||||||
@@ -140,8 +157,9 @@ export function BillFormComponent({
|
|||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input disabled={disabled} />
|
<Input disabled={disabled || disableInvNumber} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.date")}
|
label={t("bills.fields.date")}
|
||||||
name="date"
|
name="date"
|
||||||
@@ -191,9 +209,8 @@ export function BillFormComponent({
|
|||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
<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) => (
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||||
<Select.Option key={idx} value={loc}>
|
<Select.Option key={idx} value={loc}>
|
||||||
{loc}
|
{loc}
|
||||||
@@ -212,7 +229,6 @@ export function BillFormComponent({
|
|||||||
responsibilityCenters={responsibilityCenters}
|
responsibilityCenters={responsibilityCenters}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="upload"
|
name="upload"
|
||||||
label="Upload"
|
label="Upload"
|
||||||
@@ -229,7 +245,6 @@ export function BillFormComponent({
|
|||||||
<Button>Click to upload</Button>
|
<Button>Click to upload</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => {
|
||||||
const values = form.getFieldsValue([
|
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 React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -11,7 +11,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop,
|
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 { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
|
||||||
|
|
||||||
const [loadLines, { data: lineData }] = useLazyQuery(
|
const [loadLines, { data: lineData }] = useLazyQuery(
|
||||||
@@ -29,6 +35,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
|
|||||||
loadLines={loadLines}
|
loadLines={loadLines}
|
||||||
lineData={lineData ? lineData.joblines : []}
|
lineData={lineData ? lineData.joblines : []}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
|
disableInvNumber={disableInvNumber}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,24 @@ import {
|
|||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 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 FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.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,
|
disabled,
|
||||||
lineData,
|
lineData,
|
||||||
discount,
|
discount,
|
||||||
@@ -22,7 +35,7 @@ export default function BillEnterModalLinesComponent({
|
|||||||
responsibilityCenters,
|
responsibilityCenters,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.List name="billlines">
|
<Form.List name="billlines">
|
||||||
@@ -104,7 +117,7 @@ export default function BillEnterModalLinesComponent({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("billlines.fields.actual")}
|
label={t("billlines.fields.actual_price")}
|
||||||
key={`${index}actual_price`}
|
key={`${index}actual_price`}
|
||||||
name={[field.name, "actual_price"]}
|
name={[field.name, "actual_price"]}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -127,8 +140,12 @@ export default function BillEnterModalLinesComponent({
|
|||||||
...item,
|
...item,
|
||||||
actual_cost: !!item.actual_cost
|
actual_cost: !!item.actual_cost
|
||||||
? item.actual_cost
|
? item.actual_cost
|
||||||
: parseFloat(e.target.value) *
|
: Math.round(
|
||||||
(1 - discount),
|
(parseFloat(e.target.value) *
|
||||||
|
(1 - discount) +
|
||||||
|
Number.EPSILON) *
|
||||||
|
100
|
||||||
|
) / 100,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
@@ -212,6 +229,125 @@ export default function BillEnterModalLinesComponent({
|
|||||||
>
|
>
|
||||||
<Switch disabled={disabled} />
|
<Switch disabled={disabled} />
|
||||||
</Form.Item>
|
</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>
|
</LayoutFormRow>
|
||||||
<FormListMoveArrows
|
<FormListMoveArrows
|
||||||
move={move}
|
move={move}
|
||||||
@@ -246,3 +382,8 @@ export default function BillEnterModalLinesComponent({
|
|||||||
</Form.List>
|
</Form.List>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BillEnterModalLinesComponent);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const CalculateBillTotal = (invoice) => {
|
|||||||
local_tax_rate,
|
local_tax_rate,
|
||||||
state_tax_rate,
|
state_tax_rate,
|
||||||
} = invoice;
|
} = invoice;
|
||||||
|
|
||||||
//TODO Determine why this recalculates so many times.
|
//TODO Determine why this recalculates so many times.
|
||||||
let subtotal = Dinero({ amount: 0 });
|
let subtotal = Dinero({ amount: 0 });
|
||||||
let federalTax = Dinero({ amount: 0 });
|
let federalTax = Dinero({ amount: 0 });
|
||||||
@@ -22,8 +23,10 @@ export const CalculateBillTotal = (invoice) => {
|
|||||||
billlines.forEach((i) => {
|
billlines.forEach((i) => {
|
||||||
if (!!i) {
|
if (!!i) {
|
||||||
const itemTotal = Dinero({
|
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);
|
}).multiply(i.quantity || 1);
|
||||||
|
|
||||||
subtotal = subtotal.add(itemTotal);
|
subtotal = subtotal.add(itemTotal);
|
||||||
if (i.applicable_taxes.federal) {
|
if (i.applicable_taxes.federal) {
|
||||||
federalTax = federalTax.add(
|
federalTax = federalTax.add(
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
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 queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -10,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//jobRO: selectJobReadOnly,
|
//jobRO: selectJobReadOnly,
|
||||||
@@ -106,7 +115,7 @@ export function BillsListTableComponent({
|
|||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<div>
|
<Space>
|
||||||
{record.exported ? (
|
{record.exported ? (
|
||||||
<Button disabled>{t("bills.actions.edit")}</Button>
|
<Button disabled>{t("bills.actions.edit")}</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -116,7 +125,8 @@ export function BillsListTableComponent({
|
|||||||
<Button>{t("bills.actions.edit")}</Button>
|
<Button>{t("bills.actions.edit")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
<BillDeleteButton bill={record} />
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -136,7 +146,7 @@ export function BillsListTableComponent({
|
|||||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.retail"),
|
title: t("billlines.fields.actual_price"),
|
||||||
dataIndex: "actual_price",
|
dataIndex: "actual_price",
|
||||||
key: "actual_price",
|
key: "actual_price",
|
||||||
sorter: (a, b) => a.actual_price - b.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] &&
|
(selectedBillLinesByBill[record.id] &&
|
||||||
selectedBillLinesByBill[record.id].length === 0) ||
|
selectedBillLinesByBill[record.id].length === 0) ||
|
||||||
record.is_credit_memo ||
|
record.is_credit_memo
|
||||||
record.exported
|
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPartsOrderContext({
|
setPartsOrderContext({
|
||||||
@@ -266,8 +275,8 @@ export function BillsListTableComponent({
|
|||||||
.map((i) => {
|
.map((i) => {
|
||||||
return {
|
return {
|
||||||
line_desc: i.line_desc,
|
line_desc: i.line_desc,
|
||||||
db_price: i.actual_price,
|
// db_price: i.actual_price,
|
||||||
act_price: i.actual_cost,
|
act_price: i.actual_price,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
joblineid: i.joblineid,
|
joblineid: i.joblineid,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/client";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { Table, Input } from "antd";
|
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 React from "react";
|
||||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -11,6 +11,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
chatVisible: selectChatVisible,
|
chatVisible: selectChatVisible,
|
||||||
@@ -27,6 +28,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
<div>
|
<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 React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -6,6 +7,7 @@ import { setSelectedConversation } from "../../redux/messaging/messaging.actions
|
|||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
@@ -21,6 +23,8 @@ export function ChatConversationListComponent({
|
|||||||
selectedConversation,
|
selectedConversation,
|
||||||
setSelectedConversation,
|
setSelectedConversation,
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<List
|
<List
|
||||||
@@ -39,11 +43,19 @@ export function ChatConversationListComponent({
|
|||||||
{item.job_conversations.length > 0 ? (
|
{item.job_conversations.length > 0 ? (
|
||||||
<div className="chat-name">
|
<div className="chat-name">
|
||||||
{item.job_conversations.map((j, idx) => (
|
{item.job_conversations.map((j, idx) => (
|
||||||
<span key={idx}>
|
<div key={idx} style={{ display: "flex" }}>
|
||||||
{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
{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 || ""
|
j.job.ownr_co_nm || ""
|
||||||
} `}
|
} `}</div>
|
||||||
</span>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
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 { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
|
|
||||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
export default function ChatConversationTitleTags({ jobConversations }) {
|
||||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
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 React, { useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
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 Icon from "@ant-design/icons";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import moment from "moment";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +46,16 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
{MessageRender(messages[index])}
|
{MessageRender(messages[index])}
|
||||||
{StatusRender(messages[index].status)}
|
{StatusRender(messages[index].status)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
@@ -72,15 +84,19 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MessageRender = (message) => {
|
const MessageRender = (message) => {
|
||||||
if (message.image) {
|
|
||||||
return (
|
return (
|
||||||
<a href={message.image_path} target="__blank">
|
<div>
|
||||||
<img alt="Received" className="message-img" src={message.image_path} />
|
{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>
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>{message.text}</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return <span>{message.text}</span>;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusRender = (status) => {
|
const StatusRender = (status) => {
|
||||||
|
|||||||
@@ -34,9 +34,10 @@
|
|||||||
//display: inline-block;
|
//display: inline-block;
|
||||||
|
|
||||||
.message-img {
|
.message-img {
|
||||||
max-width: 3rem;
|
max-width: 10rem;
|
||||||
max-height: 3rem;
|
max-height: 10rem;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
margin: 0.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, {
|
||||||
|
PhoneItemFormatterValidation,
|
||||||
|
} from "../form-items-formatted/phone-form-item.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -26,7 +28,14 @@ export function ChatNewConversation({ openChatByPhone }) {
|
|||||||
const popContent = (
|
const popContent = (
|
||||||
<div>
|
<div>
|
||||||
<Form form={form} onFinish={handleFinish}>
|
<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 />
|
<PhoneFormItem />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Input, Spin } from "antd";
|
import { Input, Spin } from "antd";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
selectMessage,
|
selectMessage,
|
||||||
} from "../../redux/messaging/messaging.selectors";
|
} from "../../redux/messaging/messaging.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.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";
|
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -36,6 +37,7 @@ function ChatSendMessageComponent({
|
|||||||
setMessage,
|
setMessage,
|
||||||
}) {
|
}) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
@@ -43,19 +45,34 @@ function ChatSendMessageComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
|
if (message === "" || !message) return;
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
|
if (selectedImages < 11) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
to: conversation.phone_num,
|
to: conversation.phone_num,
|
||||||
body: message,
|
body: message,
|
||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
|
selectedMedia: selectedImages,
|
||||||
});
|
});
|
||||||
|
setSelectedMedia(
|
||||||
|
selectedMedia.map((i) => {
|
||||||
|
return { ...i, isSelected: false };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row">
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||||
|
<ChatMediaSelector
|
||||||
|
conversation={conversation}
|
||||||
|
selectedMedia={selectedMedia}
|
||||||
|
setSelectedMedia={setSelectedMedia}
|
||||||
|
/>
|
||||||
|
<span style={{ flex: 1 }}>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
className="imex-flex-row__margin imex-flex-row__grow"
|
className="imex-flex-row__margin imex-flex-row__grow"
|
||||||
allowClear
|
allowClear
|
||||||
@@ -71,8 +88,12 @@ function ChatSendMessageComponent({
|
|||||||
if (!!!event.shiftKey) handleEnter();
|
if (!!!event.shiftKey) handleEnter();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
<SendOutlined
|
||||||
|
className="imex-flex-row__margin"
|
||||||
|
disabled={message === "" || !message}
|
||||||
|
onClick={handleEnter}
|
||||||
|
/>
|
||||||
<Spin
|
<Spin
|
||||||
style={{ display: `${isSending ? "" : "none"}` }}
|
style={{ display: `${isSending ? "" : "none"}` }}
|
||||||
indicator={
|
indicator={
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import Rate from "./rate/rate.component";
|
|||||||
import Slider from "./slider/slider.component";
|
import Slider from "./slider/slider.component";
|
||||||
import Text from "./text/text.component";
|
import Text from "./text/text.component";
|
||||||
import Textarea from "./textarea/textarea.component";
|
import Textarea from "./textarea/textarea.component";
|
||||||
|
const e = {
|
||||||
export default {
|
|
||||||
checkbox: CheckboxFormItem,
|
checkbox: CheckboxFormItem,
|
||||||
slider: Slider,
|
slider: Slider,
|
||||||
text: Text,
|
text: Text,
|
||||||
textarea: Textarea,
|
textarea: Textarea,
|
||||||
rate: Rate,
|
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 React from "react";
|
||||||
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -14,6 +14,7 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
|
|||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
kmstart: record.mileage,
|
kmstart: record.mileage,
|
||||||
dailyrate: record.dailycost,
|
dailyrate: record.dailycost,
|
||||||
|
fuelout: record.fuel,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React, { useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/client";
|
||||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -11,6 +19,8 @@ import {
|
|||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -20,7 +30,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
export function ContractConvertToRo({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
contract,
|
||||||
|
disabled,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -46,6 +61,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
part_type: "CCDR",
|
part_type: "CCDR",
|
||||||
tax_part: true,
|
tax_part: true,
|
||||||
mod_lb_hrs: 0,
|
mod_lb_hrs: 0,
|
||||||
|
db_ref: "io-ccdr",
|
||||||
// mod_lbr_ty: "PAL",
|
// mod_lbr_ty: "PAL",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -63,6 +79,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
part_type: "CCM",
|
part_type: "CCM",
|
||||||
part_qty: mileageDiff,
|
part_qty: mileageDiff,
|
||||||
tax_part: true,
|
tax_part: true,
|
||||||
|
db_ref: "io-ccm",
|
||||||
mod_lb_hrs: 0,
|
mod_lb_hrs: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -78,6 +95,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
part_qty: values.refuelqty,
|
part_qty: values.refuelqty,
|
||||||
part_type: "CCF",
|
part_type: "CCF",
|
||||||
tax_part: true,
|
tax_part: true,
|
||||||
|
db_ref: "io-ccf",
|
||||||
mod_lb_hrs: 0,
|
mod_lb_hrs: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,6 +110,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
part_qty: 1,
|
part_qty: 1,
|
||||||
part_type: "CCC",
|
part_type: "CCC",
|
||||||
tax_part: true,
|
tax_part: true,
|
||||||
|
db_ref: "io-ccc",
|
||||||
mod_lb_hrs: 0,
|
mod_lb_hrs: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -107,6 +126,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
part_type: "CCD",
|
part_type: "CCD",
|
||||||
part_qty: 1,
|
part_qty: 1,
|
||||||
tax_part: true,
|
tax_part: true,
|
||||||
|
db_ref: "io-ccd",
|
||||||
mod_lb_hrs: 0,
|
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,
|
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||||
state_tax_rate: bodyshop.bill_tax_rates.state_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,
|
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||||
clm_no: `${contract.job.clm_no}-CC`,
|
ins_co_nm: "CC",
|
||||||
clm_total: 1234, //TODO
|
converted: true,
|
||||||
|
clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
|
||||||
ownr_fn: contract.job.owner.ownr_fn,
|
ownr_fn: contract.job.owner.ownr_fn,
|
||||||
ownr_ln: contract.job.owner.ownr_ln,
|
ownr_ln: contract.job.owner.ownr_ln,
|
||||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||||
@@ -145,13 +166,69 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
data: billingLines,
|
data: billingLines,
|
||||||
},
|
},
|
||||||
parts_tax_rates: {
|
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: {
|
CCDR: {
|
||||||
prt_type: "CCDR",
|
prt_type: "CCDR",
|
||||||
prt_discp: 0,
|
prt_discp: 0,
|
||||||
prt_mktyp: false,
|
prt_mktyp: false,
|
||||||
prt_mkupp: 0,
|
prt_mkupp: 0,
|
||||||
prt_tax_in: true,
|
prt_tax_in: true,
|
||||||
prt_tax_rt: 0.07,
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||||
},
|
},
|
||||||
CCF: {
|
CCF: {
|
||||||
prt_type: "CCF",
|
prt_type: "CCF",
|
||||||
@@ -159,7 +236,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
prt_mktyp: false,
|
prt_mktyp: false,
|
||||||
prt_mkupp: 0,
|
prt_mkupp: 0,
|
||||||
prt_tax_in: true,
|
prt_tax_in: true,
|
||||||
prt_tax_rt: 0.07,
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||||
},
|
},
|
||||||
CCM: {
|
CCM: {
|
||||||
prt_type: "CCM",
|
prt_type: "CCM",
|
||||||
@@ -167,7 +244,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
prt_mktyp: false,
|
prt_mktyp: false,
|
||||||
prt_mkupp: 0,
|
prt_mkupp: 0,
|
||||||
prt_tax_in: true,
|
prt_tax_in: true,
|
||||||
prt_tax_rt: 0.07,
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||||
},
|
},
|
||||||
CCC: {
|
CCC: {
|
||||||
prt_type: "CCC",
|
prt_type: "CCC",
|
||||||
@@ -175,7 +252,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
prt_mktyp: false,
|
prt_mktyp: false,
|
||||||
prt_mkupp: 0,
|
prt_mkupp: 0,
|
||||||
prt_tax_in: true,
|
prt_tax_in: true,
|
||||||
prt_tax_rt: 0.07,
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||||
},
|
},
|
||||||
CCD: {
|
CCD: {
|
||||||
prt_type: "CCD",
|
prt_type: "CCD",
|
||||||
@@ -183,10 +260,22 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
prt_mktyp: false,
|
prt_mktyp: false,
|
||||||
prt_mkupp: 0,
|
prt_mkupp: 0,
|
||||||
prt_tax_in: true,
|
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({
|
const result = await insertJob({
|
||||||
variables: { job: [newJob] },
|
variables: { job: [newJob] },
|
||||||
// refetchQueries: ["GET_JOB_BY_PK"],
|
// refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
@@ -244,12 +333,14 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
>
|
>
|
||||||
<InputNumber precision={0} min={0} />
|
<InputNumber precision={0} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
{t("contracts.actions.convertoro")}
|
{t("contracts.actions.convertoro")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisible(false)}>
|
<Button onClick={() => setVisible(false)}>
|
||||||
{t("general.actions.close")}
|
{t("general.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -257,7 +348,11 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Popover content={popContent} visible={visible}>
|
<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")}
|
{t("contracts.actions.convertoro")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
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 FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.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";
|
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
||||||
|
import InputPhone, {
|
||||||
export default function ContractFormComponent({ form, create = false }) {
|
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();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -46,12 +53,6 @@ export default function ContractFormComponent({ form, create = false }) {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.scheduledreturn")}
|
label={t("contracts.fields.scheduledreturn")}
|
||||||
name="scheduledreturn"
|
name="scheduledreturn"
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -84,6 +85,33 @@ export default function ContractFormComponent({ form, create = false }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</LayoutFormRow>
|
</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>
|
<LayoutFormRow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.driver_dlnumber")}
|
label={t("contracts.fields.driver_dlnumber")}
|
||||||
@@ -163,40 +191,16 @@ export default function ContractFormComponent({ form, create = false }) {
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("contracts.fields.driver_city")} name="driver_city">
|
||||||
label={t("contracts.fields.driver_city")}
|
|
||||||
name="driver_city"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("contracts.fields.driver_state")}
|
label={t("contracts.fields.driver_state")}
|
||||||
name="driver_state"
|
name="driver_state"
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("contracts.fields.driver_zip")} name="driver_zip">
|
||||||
label={t("contracts.fields.driver_zip")}
|
|
||||||
name="driver_zip"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -207,62 +211,17 @@ export default function ContractFormComponent({ form, create = false }) {
|
|||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required"),
|
message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputPhone />
|
<InputPhone />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
|
||||||
label={t("contracts.fields.driver_dob")}
|
|
||||||
name="driver_dob"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</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>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
||||||
<InputNumber precision={2} />
|
<InputNumber precision={2} />
|
||||||
|
|||||||
@@ -23,17 +23,13 @@ export default function ContractsJobsComponent({
|
|||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
key: "ro_number",
|
key: "ro_number",
|
||||||
width: "8%",
|
width: "8%",
|
||||||
sorter: (a, b) =>
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
alphaSort(
|
|
||||||
a.ro_number ? a.ro_number : "EST-" + a.est_number,
|
|
||||||
b.ro_number ? b.ro_number : "EST-" + b.est_number
|
|
||||||
),
|
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span>
|
<span>
|
||||||
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
|
{record.ro_number ? record.ro_number : t("general.labels.na")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -135,10 +131,6 @@ export default function ContractsJobsComponent({
|
|||||||
? data
|
? data
|
||||||
: data.filter(
|
: data.filter(
|
||||||
(j) =>
|
(j) =>
|
||||||
(j.est_number || "")
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(j.ro_number || "")
|
(j.ro_number || "")
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
|
|||||||
//sortOrder:
|
//sortOrder:
|
||||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Link
|
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||||
to={`/manage/courtesycars/${record.courtesycar.id}`}
|
record.courtesycar.year
|
||||||
>{`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`}</Link>
|
} ${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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-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 FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
|
|
||||||
export default function CourtesyCarReturnModalComponent() {
|
export default function CourtesyCarReturnModalComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -15,8 +15,8 @@ export default function CourtesyCarReturnModalComponent() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
@@ -27,20 +27,20 @@ export default function CourtesyCarReturnModalComponent() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.fuel")}
|
label={t("contracts.fields.fuelin")}
|
||||||
name={["courtesycar", "data", "fuel"]}
|
name={"fuelin"}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required")
|
message: t("general.validation.required"),
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CourtesyCarFuelSlider />
|
<CourtesyCarFuelSlider />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
|
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
|
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
|
||||||
import { useMutation } from "@apollo/react-hooks";
|
import { useMutation } from "@apollo/client";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
courtesyCarReturnModal: selectCourtesyCarReturn,
|
courtesyCarReturnModal: selectCourtesyCarReturn,
|
||||||
@@ -39,11 +39,12 @@ export function BillEnterModalContainer({
|
|||||||
kmend: values.kmend,
|
kmend: values.kmend,
|
||||||
actualreturn: values.actualreturn,
|
actualreturn: values.actualreturn,
|
||||||
status: "contracts.status.returned",
|
status: "contracts.status.returned",
|
||||||
|
fuelin: values.fuelin,
|
||||||
},
|
},
|
||||||
courtesycarid: context.courtesyCarId,
|
courtesycarid: context.courtesyCarId,
|
||||||
courtesycar: {
|
courtesycar: {
|
||||||
status: "courtesycars.status.in",
|
status: "courtesycars.status.in",
|
||||||
fuel: values.fuel,
|
fuel: values.fuelin,
|
||||||
mileage: values.kmend,
|
mileage: values.kmend,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { Select } from "antd";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const CourtesyCarStatusComponent = (
|
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||||
{ value = "courtesycars.status.in", onChange },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const [option, setOption] = useState(value);
|
const [option, setOption] = useState(value);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
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"),
|
title: t("courtesycars.labels.outwith"),
|
||||||
dataIndex: "outwith",
|
dataIndex: "outwith",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect } from "react";
|
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),
|
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
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) => (
|
render: (text, record) => (
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>
|
<Link to={"/manage/jobs/" + record.job.id}>
|
||||||
{record.job.est_number}
|
{record.job.ro_number || t("general.labels.na")}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// import Icon from "@ant-design/icons";
|
// import Icon from "@ant-design/icons";
|
||||||
// import { Button, Dropdown, Menu, notification } from "antd";
|
// import { Button, Dropdown, Menu, notification } from "antd";
|
||||||
// import React, { useState } from "react";
|
// 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 { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
// import { useTranslation } from "react-i18next";
|
// import { useTranslation } from "react-i18next";
|
||||||
// import { MdClose } from "react-icons/md";
|
// import { MdClose } from "react-icons/md";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { Button, Upload } from "antd";
|
import { Upload } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -15,6 +15,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function DocumentsUploadComponent({
|
export function DocumentsUploadComponent({
|
||||||
|
children,
|
||||||
currentUser,
|
currentUser,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobId,
|
jobId,
|
||||||
@@ -23,7 +24,7 @@ export function DocumentsUploadComponent({
|
|||||||
callbackAfterUpload,
|
callbackAfterUpload,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Upload
|
<Upload.Dragger
|
||||||
multiple={true}
|
multiple={true}
|
||||||
customRequest={(ev) =>
|
customRequest={(ev) =>
|
||||||
handleUpload(ev, {
|
handleUpload(ev, {
|
||||||
@@ -36,12 +37,24 @@ export function DocumentsUploadComponent({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||||
showUploadList={false}
|
// showUploadList={false}
|
||||||
>
|
>
|
||||||
<Button type="primary">
|
{
|
||||||
|
// <Button type="primary">
|
||||||
|
// <UploadOutlined />
|
||||||
|
// </Button>
|
||||||
|
}
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
</Button>
|
</p>
|
||||||
</Upload>
|
<p className="ant-upload-text">
|
||||||
|
Click or drag files to this area to upload.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Upload.Dragger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(DocumentsUploadComponent);
|
export default connect(mapStateToProps, null)(DocumentsUploadComponent);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import client, { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
|
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||||
|
import client from "../../utils/GraphQLClient";
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
|
|
||||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||||
@@ -19,8 +20,10 @@ export const handleUpload = (ev, context) => {
|
|||||||
const { bodyshop, jobId } = context;
|
const { bodyshop, jobId } = context;
|
||||||
|
|
||||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
||||||
|
let extension = ev.file.name.split(".").pop();
|
||||||
uploadToCloudinary(
|
uploadToCloudinary(
|
||||||
key,
|
key,
|
||||||
|
extension,
|
||||||
ev.file.type,
|
ev.file.type,
|
||||||
ev.file,
|
ev.file,
|
||||||
onError,
|
onError,
|
||||||
@@ -32,6 +35,7 @@ export const handleUpload = (ev, context) => {
|
|||||||
|
|
||||||
export const uploadToCloudinary = async (
|
export const uploadToCloudinary = async (
|
||||||
key,
|
key,
|
||||||
|
extension,
|
||||||
fileType,
|
fileType,
|
||||||
file,
|
file,
|
||||||
onError,
|
onError,
|
||||||
@@ -50,12 +54,16 @@ export const uploadToCloudinary = async (
|
|||||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||||
|
|
||||||
//Get the signed url.
|
//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", {
|
const signedURLResponse = await axios.post("/media/sign", {
|
||||||
public_id: public_id,
|
public_id: public_id,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
upload_preset: "incoming_upload",
|
upload_preset: upload_preset,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (signedURLResponse.status !== 200) {
|
if (signedURLResponse.status !== 200) {
|
||||||
@@ -79,8 +87,8 @@ export const uploadToCloudinary = async (
|
|||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
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("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
||||||
formData.append("public_id", public_id);
|
formData.append("public_id", public_id);
|
||||||
@@ -88,9 +96,10 @@ export const uploadToCloudinary = async (
|
|||||||
formData.append("timestamp", timestamp);
|
formData.append("timestamp", timestamp);
|
||||||
formData.append("signature", signature);
|
formData.append("signature", signature);
|
||||||
|
|
||||||
//Upload request to Cloudinary
|
|
||||||
const cloudinaryUploadResponse = await cleanAxios.post(
|
const cloudinaryUploadResponse = await cleanAxios.post(
|
||||||
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
|
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(
|
||||||
|
fileType
|
||||||
|
)}/upload`,
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
@@ -118,11 +127,13 @@ export const uploadToCloudinary = async (
|
|||||||
variables: {
|
variables: {
|
||||||
docInput: [
|
docInput: [
|
||||||
{
|
{
|
||||||
jobid: jobId,
|
...(jobId ? { jobid: jobId } : {}),
|
||||||
|
...(billId ? { billid: billId } : {}),
|
||||||
uploaded_by: uploaded_by,
|
uploaded_by: uploaded_by,
|
||||||
key: key,
|
key: key,
|
||||||
billid: billId,
|
|
||||||
type: fileType,
|
type: fileType,
|
||||||
|
extension: extension,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -145,9 +156,19 @@ export const uploadToCloudinary = async (
|
|||||||
if (!!onError) onError(JSON.stringify(documentInsert.errors));
|
if (!!onError) onError(JSON.stringify(documentInsert.errors));
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: i18n.t("documents.errors.insert", {
|
message: i18n.t("documents.errors.insert", {
|
||||||
message: JSON.stringify(JSON.stringify(documentInsert.errors)),
|
message: JSON.stringify(documentInsert.errors),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return;
|
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 { UploadOutlined } from "@ant-design/icons";
|
||||||
import { Editor } from "@tinymce/tinymce-react";
|
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 React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function EmailOverlayComponent({
|
export default function EmailOverlayComponent({
|
||||||
messageOptions,
|
messageOptions,
|
||||||
@@ -11,6 +12,7 @@ export default function EmailOverlayComponent({
|
|||||||
handleUpload,
|
handleUpload,
|
||||||
handleFileRemove,
|
handleFileRemove,
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
To:
|
To:
|
||||||
@@ -18,22 +20,28 @@ export default function EmailOverlayComponent({
|
|||||||
name="to"
|
name="to"
|
||||||
mode="tags"
|
mode="tags"
|
||||||
value={messageOptions.to}
|
value={messageOptions.to}
|
||||||
style={{ width: "100%" }}
|
//style={{ width: "100%" }}
|
||||||
onChange={handleToChange}
|
onChange={handleToChange}
|
||||||
tokenSeparators={[",", ";"]}
|
tokenSeparators={[",", ";"]}
|
||||||
/>
|
/>
|
||||||
CC:
|
CC:
|
||||||
<Input
|
<Select
|
||||||
value={messageOptions.cc}
|
value={messageOptions.cc}
|
||||||
onChange={handleConfigChange}
|
mode="tags"
|
||||||
|
onChange={(value) => handleConfigChange("cc", value)}
|
||||||
name="cc"
|
name="cc"
|
||||||
|
tokenSeparators={[",", ";"]}
|
||||||
/>
|
/>
|
||||||
Subject:
|
Subject:
|
||||||
<Input
|
<Input
|
||||||
value={messageOptions.subject}
|
value={messageOptions.subject}
|
||||||
onChange={handleConfigChange}
|
onChange={(e) => handleConfigChange("subject", e.target.value)}
|
||||||
name="subject"
|
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
|
<Editor
|
||||||
value={messageOptions.html}
|
value={messageOptions.html}
|
||||||
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
|
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
|
||||||
@@ -53,17 +61,20 @@ export default function EmailOverlayComponent({
|
|||||||
}}
|
}}
|
||||||
onEditorChange={handleHtmlChange}
|
onEditorChange={handleHtmlChange}
|
||||||
/>
|
/>
|
||||||
|
<Card title={t("emails.labels.attachments")}>
|
||||||
<Upload
|
<Upload
|
||||||
fileList={messageOptions.fileList}
|
fileList={messageOptions.fileList}
|
||||||
beforeUpload={handleUpload}
|
beforeUpload={handleUpload}
|
||||||
onRemove={handleFileRemove}
|
onRemove={handleFileRemove}
|
||||||
multiple
|
multiple
|
||||||
listType="picture-card"
|
listType="picture-card"
|
||||||
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<UploadOutlined /> Upload
|
<UploadOutlined /> Upload
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function EmailOverlayContainer({
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
const [rawHtml, setRawHtml] = useState("");
|
||||||
const defaultEmailFrom = {
|
const defaultEmailFrom = {
|
||||||
from: {
|
from: {
|
||||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
||||||
@@ -65,7 +66,11 @@ export function EmailOverlayContainer({
|
|||||||
|
|
||||||
setSending(true);
|
setSending(true);
|
||||||
try {
|
try {
|
||||||
await axios.post("/sendemail", { ...messageOptions, attachments });
|
await axios.post("/sendemail", {
|
||||||
|
...messageOptions,
|
||||||
|
html: rawHtml,
|
||||||
|
attachments,
|
||||||
|
});
|
||||||
notification["success"]({ message: t("emails.successes.sent") });
|
notification["success"]({ message: t("emails.successes.sent") });
|
||||||
toggleEmailOverlayVisible();
|
toggleEmailOverlayVisible();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -77,8 +82,7 @@ export function EmailOverlayContainer({
|
|||||||
setSending(false);
|
setSending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigChange = (event) => {
|
const handleConfigChange = (name, value) => {
|
||||||
const { name, value } = event.target;
|
|
||||||
setMessageOptions({ ...messageOptions, [name]: value });
|
setMessageOptions({ ...messageOptions, [name]: value });
|
||||||
};
|
};
|
||||||
const handleHtmlChange = (text) => {
|
const handleHtmlChange = (text) => {
|
||||||
@@ -109,11 +113,19 @@ export function EmailOverlayContainer({
|
|||||||
const render = async () => {
|
const render = async () => {
|
||||||
logImEXEvent("email_render_template", { template: emailConfig.template });
|
logImEXEvent("email_render_template", { template: emailConfig.template });
|
||||||
setLoading(true);
|
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({
|
setMessageOptions({
|
||||||
...emailConfig.messageOptions,
|
...emailConfig.messageOptions,
|
||||||
...defaultEmailFrom,
|
...defaultEmailFrom,
|
||||||
html: html,
|
html: response.data,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
});
|
});
|
||||||
setLoading(false);
|
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 React, { forwardRef, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -35,18 +35,16 @@ const EmployeeSearchSelect = (
|
|||||||
key={o.id}
|
key={o.id}
|
||||||
value={o.id}
|
value={o.id}
|
||||||
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
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}`}
|
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||||
<Tag color="blue">{o.cost_center}</Tag>
|
|
||||||
|
|
||||||
<Tag color="green">
|
<Tag color="green">
|
||||||
{o.flat_rate
|
{o.flat_rate
|
||||||
? t("timetickets.labels.flat_rate")
|
? t("timetickets.labels.flat_rate")
|
||||||
: t("timetickets.labels.straight_time")}
|
: t("timetickets.labels.straight_time")}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { withApollo } from "@apollo/client/react/hoc";
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { withApollo } from "react-apollo";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
|
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
|
||||||
import { UPDATE_FCM_TOKEN } from "../../graphql/user.queries";
|
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -15,17 +14,17 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
class FcmNotificationComponent extends Component {
|
class FcmNotificationComponent extends Component {
|
||||||
async componentDidMount() {
|
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.
|
if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
|
||||||
|
|
||||||
messaging
|
messaging
|
||||||
.requestPermission()
|
.requestPermission()
|
||||||
.then(async function () {
|
.then(async function () {
|
||||||
const token = await messaging.getToken();
|
// const token = await messaging.getToken();
|
||||||
client.mutate({
|
// client.mutate({
|
||||||
mutation: UPDATE_FCM_TOKEN,
|
// mutation: UPDATE_FCM_TOKEN,
|
||||||
variables: { authEmail: currentUser.email, token: { [token]: true } },
|
// variables: { authEmail: currentUser.email, token: { [token]: true } },
|
||||||
});
|
// });
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
console.log("Unable to get permission to notify.", err);
|
console.log("Unable to get permission to notify.", err);
|
||||||
@@ -44,22 +43,19 @@ export default connect(
|
|||||||
)(withApollo(FcmNotificationComponent));
|
)(withApollo(FcmNotificationComponent));
|
||||||
|
|
||||||
//Firebase Service Worker Register
|
//Firebase Service Worker Register
|
||||||
if ("serviceWorker" in navigator) {
|
// if ("serviceWorker" in navigator) {
|
||||||
navigator.serviceWorker
|
// navigator.serviceWorker
|
||||||
.register("/firebase-messaging-sw.js")
|
// .register("/firebase-messaging-sw.js")
|
||||||
.then(function (registration) {
|
// .then(function (registration) {
|
||||||
console.log(
|
// console.log(
|
||||||
"[FCM] Registration successful, scope is:",
|
// "[FCM] Registration successful, scope is:",
|
||||||
registration.scope
|
// registration.scope
|
||||||
);
|
// );
|
||||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
// navigator.serviceWorker.addEventListener("message", (event) => {
|
||||||
console.log("Handler for Navigator Service Worker.", event);
|
// console.log("Handler for Navigator Service Worker.", event);
|
||||||
});
|
// });
|
||||||
})
|
// })
|
||||||
.catch(function (err) {
|
// .catch(function (err) {
|
||||||
console.log(
|
// console.log("[FCM] Service worker registration failed, error:", err);
|
||||||
"[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";
|
import moment from "moment";
|
||||||
//To be used as a form element only.
|
//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) => {
|
// const handleChange = (newDate) => {
|
||||||
// if (value !== newDate && onChange) {
|
// if (value !== newDate && onChange) {
|
||||||
// onChange(newDate);
|
// onChange(newDate);
|
||||||
@@ -14,7 +14,7 @@ const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id={id}>
|
||||||
<FormDatePicker
|
<FormDatePicker
|
||||||
{...restProps}
|
{...restProps}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const FormInputNUmberCalculator = (
|
|||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
console.log("e :>> ", e.currentTarget.value);
|
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
let action;
|
let action;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
|||||||
@@ -1,7 +1,50 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React, { forwardRef } from "react";
|
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) {
|
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 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 React from "react";
|
||||||
import { UpOutlined, DownOutlined } from "@ant-design/icons";
|
|
||||||
export default function FormListMoveArrows({ move, index, total }) {
|
export default function FormListMoveArrows({ move, index, total }) {
|
||||||
const upDisabled = index === 0;
|
const upDisabled = index === 0;
|
||||||
const downDisabled = index === total - 1;
|
const downDisabled = index === total - 1;
|
||||||
@@ -9,7 +9,7 @@ export default function FormListMoveArrows({ move, index, total }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDown = () => {
|
const handleDown = () => {
|
||||||
move(index, index - 1);
|
move(index, index + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useLazyQuery } from "@apollo/react-hooks";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { AutoComplete, Input } from "antd";
|
import { AutoComplete, Input } from "antd";
|
||||||
|
import _ from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -14,27 +15,18 @@ export default function GlobalSearch() {
|
|||||||
GLOBAL_SEARCH_QUERY
|
GLOBAL_SEARCH_QUERY
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = (searchTerm) => {
|
const executeSearch = (v) => {
|
||||||
logImEXEvent("global_search", { term: searchTerm });
|
callSearch(v);
|
||||||
|
};
|
||||||
|
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||||
|
|
||||||
if (searchTerm.length > 0)
|
const handleSearch = (value) => {
|
||||||
callSearch({ variables: { search: searchTerm } });
|
if (value && value !== "")
|
||||||
|
debouncedExecuteSearch({ variables: { search: value } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTitle = (title) => {
|
const renderTitle = (title) => {
|
||||||
return (
|
return <span>{title}</span>;
|
||||||
<span>
|
|
||||||
{title}
|
|
||||||
<a
|
|
||||||
style={{ float: "right" }}
|
|
||||||
href="https://www.google.com/search?q=antd"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
more
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = data
|
const options = data
|
||||||
@@ -48,11 +40,7 @@ export default function GlobalSearch() {
|
|||||||
<Link to={`/manage/jobs/${job.id}`}>
|
<Link to={`/manage/jobs/${job.id}`}>
|
||||||
<div className="imex-flex-row">
|
<div className="imex-flex-row">
|
||||||
<span className="imex-flex-row__margin-large">
|
<span className="imex-flex-row__margin-large">
|
||||||
<strong>
|
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
||||||
{job.ro_number
|
|
||||||
? `${job.ro_number || ""} / ${job.est_number || ""}`
|
|
||||||
: `${job.est_number || ""}`}
|
|
||||||
</strong>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="imex-flex-row__margin-large">{`${
|
<span className="imex-flex-row__margin-large">{`${
|
||||||
@@ -154,14 +142,13 @@ export default function GlobalSearch() {
|
|||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
style={{ width: 200 }}
|
style={{ flex: 2 }}
|
||||||
options={options}
|
options={options}
|
||||||
|
onSearch={handleSearch}
|
||||||
>
|
>
|
||||||
<Input.Search loading={loading} onSearch={handleSearch} />
|
<Input.Search loading={loading} />
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Icon, {
|
|||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
ScheduleOutlined,
|
ScheduleOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
ToolFilled,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
ToolFilled,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Avatar, Menu } from "antd";
|
import { Avatar, Menu } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -35,8 +35,6 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
import "./header.styles.scss";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
recentItems: selectRecentItems,
|
recentItems: selectRecentItems,
|
||||||
@@ -50,6 +48,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||||
setPaymentContext: (context) =>
|
setPaymentContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||||
|
setReportCenterContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
|
||||||
signOutStart: () => dispatch(signOutStart()),
|
signOutStart: () => dispatch(signOutStart()),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,18 +61,20 @@ function Header({
|
|||||||
setBillEnterContext,
|
setBillEnterContext,
|
||||||
setTimeTicketContext,
|
setTimeTicketContext,
|
||||||
setPaymentContext,
|
setPaymentContext,
|
||||||
|
setReportCenterContext,
|
||||||
recentItems,
|
recentItems,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
// style={{ backgroundColor: "#eee" }}
|
style={{ flex: 5 }}
|
||||||
className="header-main-menu"
|
|
||||||
selectedKeys={[selectedHeader]}
|
selectedKeys={[selectedHeader]}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
|
subMenuCloseDelay={0.3}
|
||||||
>
|
>
|
||||||
<Menu.Item key="home">
|
<Menu.Item key="home">
|
||||||
<Link to="/manage">
|
<Link to="/manage">
|
||||||
@@ -214,7 +216,7 @@ function Header({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPaymentContext({
|
setPaymentContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {},
|
context: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -224,7 +226,9 @@ function Header({
|
|||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item key="timetickets">
|
<Menu.Item key="timetickets">
|
||||||
<Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
<Link to="/manage/timetickets">
|
||||||
|
{t("menus.header.timetickets")}
|
||||||
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="entertimetickets"
|
key="entertimetickets"
|
||||||
@@ -261,13 +265,29 @@ function Header({
|
|||||||
<Menu.Item key="shop">
|
<Menu.Item key="shop">
|
||||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="temporarydocs">
|
||||||
<Menu.Item key="shop-templates">
|
<Link to="/manage/temporarydocs">
|
||||||
<Link to="/manage/shop/templates">
|
{t("menus.header.temporarydocs")}
|
||||||
{t("menus.header.shop_templates")}
|
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</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">
|
<Menu.Item key="shop-vendors">
|
||||||
<Link to="/manage/shop/vendors">
|
<Link to="/manage/shop/vendors">
|
||||||
{t("menus.header.shop_vendors")}
|
{t("menus.header.shop_vendors")}
|
||||||
@@ -277,16 +297,14 @@ function Header({
|
|||||||
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.Item>
|
</Menu>
|
||||||
<GlobalSearch />
|
<GlobalSearch />
|
||||||
</Menu.Item>
|
<Menu
|
||||||
<Menu.SubMenu title={<ClockCircleFilled />}>
|
mode="horizontal"
|
||||||
{recentItems.map((i, idx) => (
|
theme="dark"
|
||||||
<Menu.Item key={idx}>
|
selectedKeys={[selectedHeader]}
|
||||||
<Link to={i.url}>{i.label}</Link>
|
onClick={handleMenuClick}
|
||||||
</Menu.Item>
|
>
|
||||||
))}
|
|
||||||
</Menu.SubMenu>
|
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
title={
|
||||||
<div>
|
<div>
|
||||||
@@ -307,7 +325,9 @@ function Header({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentUser.displayName || t("general.labels.unknown")}
|
{currentUser.displayName ||
|
||||||
|
currentUser.email ||
|
||||||
|
t("general.labels.unknown")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -339,7 +359,15 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
</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>
|
</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>
|
<span>
|
||||||
Enter your six-digit code, then click the Start Download button
|
Enter your six-digit code, then click the Start Download button
|
||||||
below{" "}
|
below
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="Code" />
|
<input type="text" name="Code" />
|
||||||
<br />
|
<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;
|
const totals = jobTotals;
|
||||||
|
|
||||||
let billTotals = Dinero({ amount: 0 });
|
let billTotals = Dinero();
|
||||||
|
let billCms = Dinero();
|
||||||
|
let lbrAdjustments = Dinero();
|
||||||
|
|
||||||
bills.forEach((i) =>
|
bills.forEach((i) =>
|
||||||
i.billlines.forEach((il) => {
|
i.billlines.forEach((il) => {
|
||||||
|
if (!i.is_credit_memo) {
|
||||||
billTotals = billTotals.add(
|
billTotals = billTotals.add(
|
||||||
Dinero({
|
Dinero({
|
||||||
amount: Math.round(
|
amount: Math.round((il.actual_price || 0) * 100),
|
||||||
(il.actual_cost || 0) * (i.is_credit_memo ? -1 : 1) * 100
|
|
||||||
),
|
|
||||||
}).multiply(il.quantity)
|
}).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 (
|
return (
|
||||||
<div className="job-bills-totals-container">
|
<div className="job-bills-totals-container">
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("jobs.labels.partstotal")}
|
title={t("jobs.labels.rosaletotal")}
|
||||||
value={Dinero(totals.parts.parts.total).toFormat()}
|
value={totalPartsSublet.toFormat()}
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title={t("jobs.labels.subletstotal")}
|
|
||||||
value={Dinero(totals.parts.sublets.total).toFormat()}
|
|
||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.retailtotal")}
|
title={t("bills.labels.retailtotal")}
|
||||||
@@ -53,6 +72,28 @@ export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
|
|||||||
}}
|
}}
|
||||||
value={discrepancy.toFormat()}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, notification } from "antd";
|
import { Button, notification } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useMutation } from "react-apollo";
|
import { useMutation } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
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 { Button, Form, notification, Switch } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
@@ -30,6 +30,7 @@ export function JobChecklistForm({
|
|||||||
currentUser,
|
currentUser,
|
||||||
type,
|
type,
|
||||||
job,
|
job,
|
||||||
|
readOnly = false,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [intakeJob] = useMutation(UPDATE_JOB);
|
const [intakeJob] = useMutation(UPDATE_JOB);
|
||||||
@@ -57,10 +58,18 @@ export function JobChecklistForm({
|
|||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
scheduled_completion: values.scheduled_completion,
|
scheduled_completion: values.scheduled_completion,
|
||||||
}),
|
}),
|
||||||
|
...(type === "deliver" && {
|
||||||
|
actual_completion: values.actual_completion,
|
||||||
|
}),
|
||||||
[(type === "intake" && "intakechecklist") ||
|
[(type === "intake" && "intakechecklist") ||
|
||||||
(type === "deliver" && "deliverchecklist")]: {
|
(type === "deliver" && "deliverchecklist")]: {
|
||||||
...values,
|
...values,
|
||||||
|
form: formItems.map((fi) => {
|
||||||
|
return {
|
||||||
|
...fi,
|
||||||
|
value: values[fi.name],
|
||||||
|
};
|
||||||
|
}),
|
||||||
completed_by: currentUser.email,
|
completed_by: currentUser.email,
|
||||||
completed_at: new Date(),
|
completed_at: new Date(),
|
||||||
},
|
},
|
||||||
@@ -112,11 +121,21 @@ export function JobChecklistForm({
|
|||||||
scheduled_completion: job && job.scheduled_completion,
|
scheduled_completion: job && job.scheduled_completion,
|
||||||
scheduled_delivery: job && job.scheduled_delivery,
|
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")}
|
{t("checklist.labels.checklist")}
|
||||||
|
|
||||||
<ConfigFormComponents componentList={formItems} />
|
<ConfigFormComponents componentList={formItems} readOnly={readOnly} />
|
||||||
|
|
||||||
{type === "intake" && (
|
{type === "intake" && (
|
||||||
<div>
|
<div>
|
||||||
@@ -124,12 +143,14 @@ export function JobChecklistForm({
|
|||||||
name="addToProduction"
|
name="addToProduction"
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
label={t("checklist.labels.addtoproduction")}
|
label={t("checklist.labels.addtoproduction")}
|
||||||
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
disabled={readOnly}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -142,6 +163,7 @@ export function JobChecklistForm({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_delivery"
|
name="scheduled_delivery"
|
||||||
label={t("jobs.fields.scheduled_delivery")}
|
label={t("jobs.fields.scheduled_delivery")}
|
||||||
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -150,8 +172,9 @@ export function JobChecklistForm({
|
|||||||
{type === "deliver" && (
|
{type === "deliver" && (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="actualCompletion"
|
name="actual_completion"
|
||||||
label={t("jobs.fields.actual_completion")}
|
label={t("jobs.fields.actual_completion")}
|
||||||
|
disabled={readOnly}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -165,15 +188,17 @@ export function JobChecklistForm({
|
|||||||
name="removeFromProduction"
|
name="removeFromProduction"
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
label={t("checklist.labels.removefromproduction")}
|
label={t("checklist.labels.removefromproduction")}
|
||||||
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch defaultChecked={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!readOnly && (
|
||||||
<Button loading={loading} htmlType="submit">
|
<Button loading={loading} htmlType="submit">
|
||||||
{t("general.actions.submit")}
|
{t("general.actions.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Form>
|
</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 { PrinterFilled } from "@ant-design/icons";
|
||||||
import JobIntakeTemplateItem from "../job-checklist-template-item/job-checklist-template-item.component";
|
import { Button, List } from "antd";
|
||||||
import { useParams } from "react-router-dom";
|
import React, { useState } from "react";
|
||||||
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 { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||||
|
import {
|
||||||
|
GenerateDocument,
|
||||||
|
GenerateDocuments,
|
||||||
|
} from "../../../../utils/RenderTemplate";
|
||||||
|
import { TemplateList } from "../../../../utils/TemplateConstants";
|
||||||
|
const TemplateListGenerated = TemplateList();
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
export default function JobIntakeTemplateList({ templates }) {
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
|
|
||||||
export function JobIntakeTemplateList({ bodyshop, templates }) {
|
|
||||||
const { jobId } = useParams();
|
const { jobId } = useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const renderTemplate = async (templateKey) => {
|
const renderTemplate = async (templateKey) => {
|
||||||
|
setLoading(true);
|
||||||
logImEXEvent("job_checklist_template_render");
|
logImEXEvent("job_checklist_template_render");
|
||||||
|
|
||||||
const html = await RenderTemplate(
|
await GenerateDocument(
|
||||||
{
|
{
|
||||||
name: templateKey,
|
name: templateKey,
|
||||||
variables: { id: jobId },
|
variables: { id: jobId },
|
||||||
},
|
},
|
||||||
bodyshop
|
{},
|
||||||
|
"p"
|
||||||
);
|
);
|
||||||
displayTemplateInWindow(html);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAllTemplates = () => {
|
const renderAllTemplates = async () => {
|
||||||
logImEXEvent("job_checklist_render_all_templates");
|
logImEXEvent("checklist_render_all_templates");
|
||||||
|
setLoading(true);
|
||||||
templates.forEach((template) => renderTemplate(template));
|
console.log("templates :>> ", templates);
|
||||||
|
await GenerateDocuments(
|
||||||
|
templates.map((key) => {
|
||||||
|
return { name: key, variables: { id: jobId } };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{t("intake.labels.printpack")}
|
<Button onClick={renderAllTemplates} loading={loading}>
|
||||||
<Button onClick={renderAllTemplates}>
|
|
||||||
{t("checklist.actions.printall")}
|
{t("checklist.actions.printall")}
|
||||||
</Button>
|
</Button>
|
||||||
{templates.map((template) => (
|
<List
|
||||||
<JobIntakeTemplateItem
|
itemLayout="horizontal"
|
||||||
key={template}
|
dataSource={templates}
|
||||||
templateKey={template}
|
renderItem={(template) => (
|
||||||
renderTemplate={renderTemplate}
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(JobIntakeTemplateList);
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export default function JobIntakeComponent({ checklistConfig, type, job }) {
|
|||||||
const { form, templates } = checklistConfig;
|
const { form, templates } = checklistConfig;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row gutter={[48, 48]}>
|
||||||
<Col span={12}>
|
<Col span={6}>
|
||||||
<JobChecklistTemplateList templates={templates} type={type} />
|
<JobChecklistTemplateList templates={templates} type={type} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={18}>
|
||||||
<JobChecklistForm formItems={form} type={type} job={job} />
|
<JobChecklistForm formItems={form} type={type} job={job} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { Typography } from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
|
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
|
||||||
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -15,9 +18,16 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function JobCostingModalComponent({ bodyshop, job }) {
|
export function JobCostingModalComponent({ bodyshop, job }) {
|
||||||
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
||||||
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
const allProfitCenters = _.union(
|
||||||
|
bodyshop.md_responsibility_centers.profits.map((p) => p.name),
|
||||||
|
bodyshop.md_responsibility_centers.costs.map((p) => p.name)
|
||||||
|
);
|
||||||
|
|
||||||
const jobLineTotalsByProfitCenter = job.joblines.reduce(
|
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const jobLineTotalsByProfitCenter =
|
||||||
|
job &&
|
||||||
|
job.joblines.reduce(
|
||||||
(acc, val) => {
|
(acc, val) => {
|
||||||
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
|
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
|
||||||
|
|
||||||
@@ -66,6 +76,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
.multiply(line_val.quantity)
|
.multiply(line_val.quantity)
|
||||||
.multiply(bill_val.is_credit_memo ? -1 : 1)
|
.multiply(bill_val.is_credit_memo ? -1 : 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
return bill_acc;
|
return bill_acc;
|
||||||
@@ -82,7 +93,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
].add(
|
].add(
|
||||||
Dinero({
|
Dinero({
|
||||||
amount: Math.round((ticket_val.rate || 0) * 100),
|
amount: Math.round((ticket_val.rate || 0) * 100),
|
||||||
}).multiply(ticket_val.actualhrs || 0)
|
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
return ticket_acc;
|
return ticket_acc;
|
||||||
@@ -102,8 +113,8 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
gppercentFormatted: null,
|
gppercentFormatted: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const costCenterData = Object.keys(defaultProfits).map((key, idx) => {
|
const costCenterData = allProfitCenters.map((key, idx) => {
|
||||||
const ccVal = defaultProfits[key];
|
const ccVal = key; // defaultProfits[key];
|
||||||
const sale_labor =
|
const sale_labor =
|
||||||
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
|
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
|
||||||
const sale_parts =
|
const sale_parts =
|
||||||
@@ -113,11 +124,11 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||||
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||||
|
|
||||||
const cost = (billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })).add(
|
const costs = (
|
||||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
||||||
);
|
).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
|
||||||
const totalSales = sale_labor.add(sale_parts);
|
const totalSales = sale_labor.add(sale_parts);
|
||||||
const gpdollars = totalSales.subtract(cost);
|
const gpdollars = totalSales.subtract(costs);
|
||||||
const gppercent = (
|
const gppercent = (
|
||||||
(gpdollars.getAmount() / totalSales.getAmount()) *
|
(gpdollars.getAmount() / totalSales.getAmount()) *
|
||||||
100
|
100
|
||||||
@@ -137,16 +148,19 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
.add(sale_parts);
|
.add(sale_parts);
|
||||||
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
|
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
|
||||||
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
|
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
|
||||||
summaryData.totalCost = summaryData.totalCost.add(cost);
|
summaryData.totalCost = summaryData.totalCost.add(costs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: idx,
|
id: idx,
|
||||||
cost_center: ccVal,
|
cost_center: ccVal,
|
||||||
sale_labor: sale_labor && sale_labor.toFormat(),
|
sale_labor: sale_labor && sale_labor.toFormat(),
|
||||||
sale_parts: sale_parts && sale_parts.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_parts: cost_parts && cost_parts.toFormat(),
|
||||||
cost_labor: cost_labor && cost_labor.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(),
|
gpdollars: gpdollars.toFormat(),
|
||||||
gppercent: gppercentFormatted,
|
gppercent: gppercentFormatted,
|
||||||
};
|
};
|
||||||
@@ -170,7 +184,23 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<JobCostingStatistics job={job} summaryData={summaryData} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@apollo/react-hooks";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Modal } from "antd";
|
import { Modal } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -37,10 +37,11 @@ export function JobCostingModalContainer({
|
|||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title={t("jobs.labels.jobcosting")}
|
title={t("jobs.labels.jobcosting")}
|
||||||
|
onOk={() => toggleModalVisible()}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
width="90%"
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
forceRender
|
|
||||||
>
|
>
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{loading ? (
|
{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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
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({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
});
|
});
|
||||||
@@ -24,37 +25,23 @@ export default function JobCostingPartsTable({ job, data }) {
|
|||||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.labels.sale_labor"),
|
title: t("jobs.labels.sales"),
|
||||||
dataIndex: "sale_labor",
|
dataIndex: "sales",
|
||||||
key: "sale_labor",
|
key: "sales",
|
||||||
sorter: (a, b) => alphaSort(a.sale_labor, b.sale_labor),
|
sorter: (a, b) => alphaSort(a.sales, b.sales),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "sale_labor" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("jobs.labels.sale_parts"),
|
title: t("jobs.labels.costs"),
|
||||||
dataIndex: "sale_parts",
|
dataIndex: "costs",
|
||||||
key: "sale_parts",
|
key: "costs",
|
||||||
sorter: (a, b) => alphaSort(a.sale_parts, b.sale_parts),
|
sorter: (a, b) => a.costs - b.costs,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "sale_parts" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "costs" && 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,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("jobs.labels.gpdollars"),
|
title: t("jobs.labels.gpdollars"),
|
||||||
dataIndex: "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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
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" }}
|
scroll={{ x: "50%", y: "40rem" }}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user