Merged in development (pull request #18)

Merge to master.
This commit is contained in:
Patrick Fic
2021-02-26 01:36:36 +00:00
833 changed files with 143444 additions and 10811 deletions

View File

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

@@ -0,0 +1,12 @@
{
"env": {
"browser": false,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {}
}

3
.gitignore vendored
View File

@@ -10,6 +10,8 @@ client.pnp.js
admin/node_modules
admin/.pnp
admin.pnp.js
jsreport/node_modules
jsreport/auth-server/node_modules
# testing
/coverage
client/coverage
@@ -37,6 +39,7 @@ client/yarn-error.log*
admin/npm-debug.log*
admin/yarn-debug.log*
admin/yarn-error.log*
client/.eslintcache
#Firebase Ignore
# Logs

View File

@@ -12,15 +12,15 @@ To Start Hasura CLI:
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
Migrating to Staging:
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
NGROK TEsting:
./ngrok.exe http https://localhost:5000 -host-header="localhost:5000"
./ngrok.exe http http://localhost:5000 -host-header="localhost:5000"
Finding deadfiles - run from client directory
npx deadfile ./src/index.js --exclude build templates
cd client && yarn build && cd build && scp -r ** imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd ..
cd client && yarn build && cd build && scp -r \*\* imex@prod-tor1.imex.online:~/bodyshop/client/build && cd .. &&cd ..
gq https://bodyshop-dev-db.herokuapp.com/v1/graphql -H "X-Hasura-Admin-Secret: Dev-BodyShopAppBySnaptSoftware\!" --introspect > schema.graphql

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,14 @@
"apollo-link-logger": "^1.2.3",
"dotenv": "^8.2.0",
"firebase": "^7.21.0",
"graphql": "^15.3.0",
"graphql": "^15.4.0",
"prop-types": "^15.7.2",
"ra-data-hasura-graphql": "^0.1.12",
"react": "^16.13.1",
"react": "^17.0.1",
"react-admin": "^3.8.5",
"react-dom": "^16.13.1",
"react-dom": "^17.0.1",
"react-icons": "^3.11.0",
"react-scripts": "3.4.3"
"react-scripts": "4.0.0"
},
"scripts": {
"start": "set PORT=3001 && react-scripts start",

View File

@@ -5,7 +5,7 @@ const JobsCreate = (props) => (
<Create {...props}>
<SimpleForm>
<TextInput source="ro_number" />
<TextInput source="est_number" />
<TextInput source="ownr_fn" />
<TextInput source="ownr_ln" />
<TextInput source="converted" />

View File

@@ -141,7 +141,6 @@ const JobsEdit = (props) => (
<TextInput fullWidth source="date_open" />
<TextInput fullWidth source="date_scheduled" />
<TextInput fullWidth source="date_invoiced" />
<TextInput fullWidth source="date_closed" />
<TextInput fullWidth source="date_exported" />
</FormTab>
<FormTab label="Insurance info">
@@ -283,7 +282,6 @@ const JobsEdit = (props) => (
<FormTab label="Other">
<TextInput fullWidth source="area_of_damage" />
<TextInput fullWidth source="loss_cat" />
<TextInput fullWidth source="est_number" />
<TextInput fullWidth source="special_coverage_policy" />
<TextInput fullWidth source="csr" />
<TextInput fullWidth source="po_number" />

View File

@@ -1,16 +1,16 @@
import { useQuery } from "@apollo/client";
import CircularProgress from "@material-ui/core/CircularProgress";
import React from "react";
import {
Datagrid,
Filter,
List,
ReferenceField,
TextField,
SelectInput,
TextInput,
TextField,
TextInput
} from "react-admin";
import { useQuery } from "@apollo/client";
import { QUERY_ALL_SHOPS } from "../../graphql/admin.shop.queries";
import CircularProgress from "@material-ui/core/CircularProgress";
const JobsList = (props) => (
<List filters={<JobsFilter />} {...props}>
@@ -20,7 +20,7 @@ const JobsList = (props) => (
<TextField source="shopname" />
</ReferenceField>
<TextField source="ro_number" />
<TextField source="est_number" />
<TextField source="ownr_fn" />
<TextField source="ownr_ln" />
<TextField source="ownr_co_nm" />

View File

@@ -128,7 +128,6 @@ const JobsShow = (props) => (
<TextField source="date_open" />
<TextField source="date_scheduled" />
<TextField source="date_invoiced" />
<TextField source="date_closed" />
<TextField source="date_exported" />
<TextField source="clm_total" />
<TextField source="owner_owing" />
@@ -221,7 +220,7 @@ const JobsShow = (props) => (
<TextField source="ownr_ea" />
<TextField source="area_of_damage" />
<TextField source="loss_cat" />
<TextField source="est_number" />
<TextField source="special_coverage_policy" />
<TextField source="csr" />
<TextField source="po_number" />

View File

@@ -1,4 +1,4 @@
import gql from "graphql-tag";
import { gql } from "@apollo/client";
export const QUERY_ALL_SHOPS = gql`
query QUERY_ALL_SHOPS {

File diff suppressed because it is too large Load Diff

1
client/debug.log Normal file
View 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

File diff suppressed because it is too large Load Diff

45978
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,52 @@
{
"name": "bodyshop",
"version": "0.1.0001",
"version": "0.1.1",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@lourenci/react-kanban": "^2.0.0",
"@stripe/react-stripe-js": "^1.1.2",
"@stripe/stripe-js": "^1.9.0",
"@tanem/react-nprogress": "^3.0.46",
"@tinymce/tinymce-react": "^3.7.0",
"antd": "^4.6.6",
"apollo-boost": "^0.4.9",
"@apollo/client": "^3.3.11",
"@fingerprintjs/fingerprintjs": "^3.0.6",
"@lourenci/react-kanban": "^2.1.0",
"@sentry/react": "^6.2.0",
"@sentry/tracing": "^6.2.0",
"@stripe/react-stripe-js": "^1.2.2",
"@stripe/stripe-js": "^1.12.1",
"@tanem/react-nprogress": "^3.0.56",
"@tinymce/tinymce-react": "^3.10.2",
"antd": "^4.12.3",
"apollo-link-logger": "^2.0.0",
"axios": "^0.20.0",
"codemirror": "^5.58.1",
"codemirror-graphql": "^0.12.2",
"axios": "^0.21.1",
"dinero.js": "^1.8.1",
"dotenv": "^8.2.0",
"fingerprintjs2": "^2.1.2",
"firebase": "^7.22.1",
"graphql": "^15.3.0",
"i18next": "^19.8.2",
"firebase": "^8.2.9",
"graphql": "^15.5.0",
"i18next": "^19.8.9",
"i18next-browser-languagedetector": "^6.0.1",
"inline-css": "^2.6.3",
"jsoneditor": "^9.1.1",
"jsoneditor-react": "^3.0.1",
"jsoneditor": "^9.1.10",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.11",
"logrocket": "^1.0.13",
"moment-business-days": "^1.2.0",
"node-sass": "^4.14.1",
"phone": "^2.4.16",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.5",
"react": "^16.13.1",
"react-apollo": "^3.1.5",
"react-big-calendar": "^0.28.0",
"react-codemirror2": "^7.2.1",
"react-color": "^2.18.1",
"react-dom": "^16.13.1",
"react-drag-listview": "^0.1.7",
"react-email-editor": "^1.1.1",
"react-ga": "^3.1.2",
"query-string": "^6.14.0",
"react": "^17.0.1",
"react-big-calendar": "^0.30.0",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5",
"react-i18next": "^11.7.3",
"react-icons": "^3.11.0",
"react-moment": "^1.0.0",
"react-number-format": "^4.4.1",
"react-redux": "^7.2.1",
"react-i18next": "^11.8.7",
"react-icons": "^4.2.0",
"react-number-format": "^4.4.4",
"react-phone-input-2": "^2.13.9",
"react-redux": "^7.2.2",
"react-resizable": "^1.11.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"react-trello": "^2.2.8",
"react-virtualized": "^9.22.2",
"recharts": "^1.8.5",
"react-scripts": "^4.0.3",
"react-virtualized": "^9.22.3",
"recharts": "^2.0.7",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
@@ -64,6 +59,7 @@
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-scripts start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
"build-deploy": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build && s3cmd sync build/* s3://imex-online-production && echo '🚀 Deployed!'",
"test": "react-scripts test",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
@@ -84,10 +80,7 @@
]
},
"devDependencies": {
"@apollo/react-testing": "^4.0.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.0"
"source-map-explorer": "^2.5.2"
}
}

BIN
client/public/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -5,10 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<meta name="description" content="ImEX Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="logo192.png" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

@@ -1,4 +1,4 @@
import { ApolloProvider } from "@apollo/react-common";
import { ApolloProvider } from "@apollo/client";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import LogRocket from "logrocket";

View File

@@ -7,6 +7,7 @@ import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import AboutPage from "../pages/about/about.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
@@ -62,6 +63,9 @@ export function App({ checkUserSession, currentUser }) {
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/about" component={AboutPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
exact

View File

@@ -49,25 +49,77 @@
}
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 0.2rem;
background-color: #f5f5f5;
}
// ::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// border-radius: 0.2rem;
// background-color: #f5f5f5;
// }
::-webkit-scrollbar {
width: 0.25rem;
max-height: 0.25rem;
background-color: #f5f5f5;
}
// ::-webkit-scrollbar {
// width: 0.25rem;
// max-height: 0.25rem;
// background-color: #f5f5f5;
// }
::-webkit-scrollbar-thumb {
border-radius: 0.2rem;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #188fff;
}
// ::-webkit-scrollbar-thumb {
// border-radius: 0.2rem;
// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
// background-color: #188fff;
// }
.ant-table-cell {
// background-color: red;
//padding: 0.2rem !important;
}
.ant-input-number-input,
.ant-input-number,
.ant-picker-input,
.ant-picker,
.ant-select {
width: 100%;
}
.production-alert {
animation: alertBlinker 1s linear infinite;
color: blue;
}
@keyframes alertBlinker {
50% {
color: red;
opacity: 100;
//opacity: 0;
}
}
.blue {
color: blue;
}
.production-completion-1 {
animation: production-completion-1-blinker 5s linear infinite;
}
@keyframes production-completion-1-blinker {
50% {
background: rgba(207, 12, 12, 0.555);
}
}
.react-resizable {
position: relative;
background-clip: padding-box;
}
.react-resizable-handle {
position: absolute;
width: 10px;
height: 100%;
bottom: 0;
right: -5px;
cursor: col-resize;
z-index: 1;
}
.production-list-min-height {
min-height: 19px;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -72,7 +72,7 @@ function Test({ bodyshop, setEmailOptions }) {
replyTo: bodyshop.email,
},
template: {
name: TemplateList().parts_order_confirmation.key,
name: TemplateList().parts_order.key,
variables: {
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
},

View File

@@ -150,19 +150,20 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
loading={loading}
title={() => {
return (
<div>
<div className="imex-table-header">
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
/>
<Input
className="imex-table-header__search"
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
/>
<PayableExportAll
billIds={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
/>
</div>
);
}}

View File

@@ -2,12 +2,12 @@ import { Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
export default function AccountingPayablesTableComponent({
loading,
@@ -38,17 +38,21 @@ export default function AccountingPayablesTableComponent({
),
},
{
title: t("jobs.fields.est_number"),
dataIndex: "est_number",
key: "est_number",
sorter: (a, b) => a.job.est_number - b.job.est_number,
title: t("payments.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => alphaSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.est_number}
</Link>
),
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("payments.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => alphaSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
},
{
title: t("jobs.fields.owner"),

View File

@@ -1,11 +1,11 @@
import { Input, Table, Button } from "antd";
import { Button, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { JobsExportAllButton } from "../jobs-export-all-button/jobs-export-all-button.component";
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
@@ -34,17 +34,7 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
),
},
{
title: t("jobs.fields.est_number"),
dataIndex: "est_number",
key: "est_number",
sorter: (a, b) => a.est_number - b.est_number,
sortOrder:
state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>{record.est_number}</Link>
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
@@ -156,10 +146,6 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.est_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(v.ownr_fn || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||

View File

@@ -1,6 +1,6 @@
import { Alert } from "antd";
import { logImEXEvent } from "../../firebase/firebase.utils";
import React from "react";
import { logImEXEvent } from "../../firebase/firebase.utils";
export default function AlertComponent(props) {
if (props.type === "error") logImEXEvent("alert_render", { ...props });

View File

@@ -1,17 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import ReactDOM from "react-dom";
import Alert from "./alert.component";
import { MockedProvider } from "@apollo/react-testing";
import { shallow, mount } from "enzyme";
const div = document.createElement("div");
describe("Alert component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message."
message: "Test error message.",
};
wrapper = shallow(<Alert {...mockProps} />);

View File

@@ -1,9 +1,7 @@
import { mount, shallow } from "enzyme";
import { mount } from "enzyme";
import React from "react";
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
import { MockBodyshop } from "../../utils/TestingHelpers";
import { Select } from "antd";
const div = document.createElement("div");
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
describe("AllocationsAssignmentComponent component", () => {
let wrapper;

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "@apollo/react-hooks";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
@@ -8,29 +8,29 @@ import { notification } from "antd";
export default function AllocationsAssignmentContainer({
jobLineId,
hours,
refetch
refetch,
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null
employeeid: null,
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } })
.then(r => {
.then((r) => {
notification["success"]({
message: t("allocations.successes.save")
message: t("allocations.successes.save"),
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch(error => {
.catch((error) => {
notification["error"]({
message: t("employees.errors.saving", { message: error.message })
message: t("employees.errors.saving", { message: error.message }),
});
});
};

View File

@@ -1,18 +1,18 @@
import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import { useMutation } from "@apollo/react-hooks";
import { useMutation } from "@apollo/client";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({
jobLines,
refetch
refetch,
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null
employeeid: null,
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
@@ -21,14 +21,14 @@ export default function AllocationsBulkAssignmentContainer({
acc.push({
joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid
employeeid: assignment.employeeid,
});
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then(r => {
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
notification["success"]({
message: t("employees.successes.save")
message: t("employees.successes.save"),
});
visibilityState[1](false);
if (refetch) refetch();
@@ -37,7 +37,7 @@ export default function AllocationsBulkAssignmentContainer({
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { useMutation } from "@apollo/client";
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
import { notification } from "antd";

View File

@@ -1,6 +1,6 @@
import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component";
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -15,7 +15,7 @@ export default function AuditTrailListContainer({ recordId }) {
return (
<div>
{error ? (
<AlertComponent type='error' message={error.message} />
<AlertComponent type="error" message={error.message} />
) : (
<AuditTrailListComponent
loading={loading}

View File

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

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery } from "@apollo/react-hooks";
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form } from "antd";
import moment from "moment";
import queryString from "query-string";

View File

@@ -1,19 +1,24 @@
import { useMutation } from "@apollo/react-hooks";
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Form, Modal, notification } from "antd";
import _ from "lodash";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import BillFormContainer from "../bill-form/bill-form.container";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -34,84 +39,154 @@ function BillEnterModalContainer({
const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const handleFinish = (values) => {
const handleFinish = async (values) => {
setLoading(true);
const { upload, location, ...remainingValues } = values;
insertBill({
let adjustmentsToInsert = {};
const r1 = await insertBill({
variables: {
bill: [
Object.assign({}, remainingValues, {
{
...remainingValues,
billlines: {
data:
remainingValues.billlines &&
remainingValues.billlines.map((i) => {
const {
deductfromlabor,
lbr_adjustment,
location: lineLocation,
...restI
} = i;
if (deductfromlabor) {
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
restI.actual_price / lbr_adjustment.rate;
}
return {
...i,
...restI,
deductedfromlbr: deductfromlabor,
joblineid: i.joblineid === "noline" ? null : i.joblineid,
};
}),
},
}),
},
],
},
})
.then((r) => {
const billId = r.data.insert_bills.returning[0].id;
});
console.log("adjustmentsToInsert", adjustmentsToInsert);
const adjKeys = Object.keys(adjustmentsToInsert);
if (adjKeys.length > 0) {
//Query the adjustments, merge, and update them.
const existingAdjustments = await client.query({
query: QUERY_JOB_LBR_ADJUSTMENTS,
variables: {
id: values.jobid,
},
});
updateJobLines({
variables: {
ids: remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => li.joblineid),
status: bodyshop.md_order_statuses.default_received || "Received*",
location: location,
},
}).then((joblineresult) => {
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
///////////////////////////
setLoading(false);
notification["success"]({
message: t("bills.successes.created"),
});
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
if (enterAgain) {
form.resetFields();
form.setFieldsValue({ billlines: [] });
} else {
toggleModalVisible();
}
setEnterAgain(false);
});
})
.catch((error) => {
setLoading(false);
setEnterAgain(false);
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
});
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
variables: {
jobId: values.jobid,
job: { lbr_adjustments: newAdjustments },
},
});
if (!!jobUpdate.errors) {
notification["error"]({
message: t("bills.errors.creating", {
message: JSON.stringify(error),
message: t("jobs.errors.saving", {
message: JSON.stringify(jobUpdate.errors),
}),
});
}
}
if (!!r1.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("bills.errors.creating", {
message: JSON.stringify(r1.errors),
}),
});
}
const billId = r1.data.insert_bills.returning[0].id;
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
},
},
});
})
);
// await updateJobLines({
// variables: {
// ids: remainingValues.billlines
// .filter((il) => il.joblineid !== "noline")
// .map((li) => li.joblineid),
// status: bodyshop.md_order_statuses.default_received || "Received*",
// location: location,
// },
// });
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
///////////////////////////
setLoading(false);
notification["success"]({
message: t("bills.successes.created"),
});
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
if (enterAgain) {
form.resetFields();
form.setFieldsValue({ billlines: [] });
} else {
toggleModalVisible();
}
setEnterAgain(false);
};
const handleCancel = () => {
@@ -183,7 +258,10 @@ function BillEnterModalContainer({
0,
}}
>
<BillFormContainer form={form} />
<BillFormContainer
form={form}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</Form>
</Modal>
);

View File

@@ -10,7 +10,7 @@ import {
Upload,
} from "antd";
import React, { useEffect, useState } from "react";
import { useApolloClient } from "react-apollo";
import { useApolloClient } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -18,12 +18,14 @@ import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -38,6 +40,7 @@ export function BillFormComponent({
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -47,7 +50,6 @@ export function BillFormComponent({
setDiscount(opt.discount);
};
//TODO: Test this further. Required to set discount when viewing an invoice.
useEffect(() => {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
const vendorId = form.getFieldValue("vendorid");
@@ -65,7 +67,15 @@ export function BillFormComponent({
return (
<div>
<LayoutFormRow>
<FormFieldsChanged form={form} />
<Form.Item
style={{ display: "none" }}
name="isinhouse"
valuePropName="checked"
>
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
@@ -78,6 +88,8 @@ export function BillFormComponent({
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
// notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
@@ -103,7 +115,6 @@ export function BillFormComponent({
/>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
@@ -118,7 +129,7 @@ export function BillFormComponent({
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid) {
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
@@ -129,6 +140,12 @@ export function BillFormComponent({
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
@@ -140,8 +157,9 @@ export function BillFormComponent({
}),
]}
>
<Input disabled={disabled} />
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
@@ -191,9 +209,8 @@ export function BillFormComponent({
>
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select style={{ width: "10rem" }} disabled={disabled}>
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
@@ -212,7 +229,6 @@ export function BillFormComponent({
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
<Form.Item
name="upload"
label="Upload"
@@ -229,7 +245,6 @@ export function BillFormComponent({
<Button>Click to upload</Button>
</Upload>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const values = form.getFieldsValue([

View File

@@ -1,4 +1,4 @@
import { useLazyQuery, useQuery } from "@apollo/react-hooks";
import { useLazyQuery, useQuery } from "@apollo/client";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -11,7 +11,13 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
export function BillFormContainer({
bodyshop,
form,
billEdit,
disabled,
disableInvNumber,
}) {
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
const [loadLines, { data: lineData }] = useLazyQuery(
@@ -29,6 +35,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
/>
);
}

View File

@@ -10,11 +10,24 @@ import {
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function BillEnterModalLinesComponent({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function BillEnterModalLinesComponent({
bodyshop,
disabled,
lineData,
discount,
@@ -22,7 +35,7 @@ export default function BillEnterModalLinesComponent({
responsibilityCenters,
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue } = form;
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
return (
<Form.List name="billlines">
@@ -104,7 +117,7 @@ export default function BillEnterModalLinesComponent({
/>
</Form.Item>
<Form.Item
label={t("billlines.fields.actual")}
label={t("billlines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
rules={[
@@ -127,8 +140,12 @@ export default function BillEnterModalLinesComponent({
...item,
actual_cost: !!item.actual_cost
? item.actual_cost
: parseFloat(e.target.value) *
(1 - discount),
: Math.round(
(parseFloat(e.target.value) *
(1 - discount) +
Number.EPSILON) *
100
) / 100,
};
}
return item;
@@ -212,6 +229,125 @@ export default function BillEnterModalLinesComponent({
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.labels.deductfromlabor")}
key={`${index}deductfromlabor`}
valuePropName="checked"
name={[field.name, "deductfromlabor"]}
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
shouldUpdate={(prev, cur) =>
prev.billlines[index] &&
prev.billlines[index].deductfromlabor !==
cur.billlines[index] &&
cur.billlines[index].deductfromlabor
}
>
{() => {
if (
getFieldValue([
"billlines",
field.name,
"deductfromlabor",
])
)
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
key={`${index}modlbrty`}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
name={[
field.name,
"lbr_adjustment",
"mod_lbr_ty",
]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={[field.name, "lbr_adjustment", "rate"]}
initialValue={
bodyshop.default_adjustment_rate
}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <span />;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.location")}
key={`${index}location`}
name={[field.name, "location"]}
>
<Select style={{ width: "10rem" }} disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
</LayoutFormRow>
<FormListMoveArrows
move={move}
@@ -246,3 +382,8 @@ export default function BillEnterModalLinesComponent({
</Form.List>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillEnterModalLinesComponent);

View File

@@ -11,6 +11,7 @@ export const CalculateBillTotal = (invoice) => {
local_tax_rate,
state_tax_rate,
} = invoice;
//TODO Determine why this recalculates so many times.
let subtotal = Dinero({ amount: 0 });
let federalTax = Dinero({ amount: 0 });
@@ -22,8 +23,10 @@ export const CalculateBillTotal = (invoice) => {
billlines.forEach((i) => {
if (!!i) {
const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100) || 0,
amount:
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
}).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal);
if (i.applicable_taxes.federal) {
federalTax = federalTax.add(

View File

@@ -1,5 +1,13 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Checkbox, Descriptions, Input, Table, Typography } from "antd";
import {
Button,
Checkbox,
Descriptions,
Input,
Space,
Table,
Typography,
} from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -10,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly,
@@ -106,7 +115,7 @@ export function BillsListTableComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<div>
<Space>
{record.exported ? (
<Button disabled>{t("bills.actions.edit")}</Button>
) : (
@@ -116,7 +125,8 @@ export function BillsListTableComponent({
<Button>{t("bills.actions.edit")}</Button>
</Link>
)}
</div>
<BillDeleteButton bill={record} />
</Space>
),
},
];
@@ -136,7 +146,7 @@ export function BillsListTableComponent({
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("billlines.fields.retail"),
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
key: "actual_price",
sorter: (a, b) => a.actual_price - b.actual_price,
@@ -248,8 +258,7 @@ export function BillsListTableComponent({
!selectedBillLinesByBill[record.id] ||
(selectedBillLinesByBill[record.id] &&
selectedBillLinesByBill[record.id].length === 0) ||
record.is_credit_memo ||
record.exported
record.is_credit_memo
}
onClick={() =>
setPartsOrderContext({
@@ -266,8 +275,8 @@ export function BillsListTableComponent({
.map((i) => {
return {
line_desc: i.line_desc,
db_price: i.actual_price,
act_price: i.actual_cost,
// db_price: i.actual_price,
act_price: i.actual_price,
quantity: i.quantity,
joblineid: i.joblineid,
};

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import { useHistory, useLocation } from "react-router-dom";
import { Table, Input } from "antd";

View File

@@ -1,4 +1,4 @@
import { useSubscription } from "@apollo/react-hooks";
import { useSubscription } from "@apollo/client";
import React from "react";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
import AlertComponent from "../alert/alert.component";
@@ -11,6 +11,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
@@ -27,6 +28,8 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
<div>

View File

@@ -1,4 +1,5 @@
import { Badge, List, Tag } from "antd";
import { Badge, List, Tag, Tooltip } from "antd";
import { AlertFilled } from "@ant-design/icons";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -6,6 +7,7 @@ import { setSelectedConversation } from "../../redux/messaging/messaging.actions
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import PhoneFormatter from "../../utils/PhoneFormatter";
import "./chat-conversation-list.styles.scss";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation,
@@ -21,6 +23,8 @@ export function ChatConversationListComponent({
selectedConversation,
setSelectedConversation,
}) {
const { t } = useTranslation();
return (
<div className="chat-list-container">
<List
@@ -39,11 +43,19 @@ export function ChatConversationListComponent({
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<span key={idx}>
{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
<div key={idx} style={{ display: "flex" }}>
{j.job.owner && !j.job.owner.allow_text_message && (
<Tooltip title={t("messaging.labels.noallowtxt")}>
<AlertFilled
className="production-alert"
style={{ marginRight: ".3rem", alignItems: "center" }}
/>
</Tooltip>
)}
<div>{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
j.job.ownr_co_nm || ""
} `}
</span>
} `}</div>
</div>
))}
</div>
) : (

View File

@@ -1,9 +1,10 @@
import React from "react";
import { useMutation } from "@apollo/client";
import { Tag } from "antd";
import React from "react";
import { Link } from "react-router-dom";
import { useMutation } from "@apollo/react-hooks";
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
export default function ChatConversationTitleTags({ jobConversations }) {
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);

View File

@@ -1,4 +1,4 @@
import { useMutation, useSubscription } from "@apollo/react-hooks";
import { useMutation, useSubscription } from "@apollo/client";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";

View File

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

View File

@@ -1,4 +1,6 @@
import Icon from "@ant-design/icons";
import i18n from "i18next";
import moment from "moment";
import React, { useEffect, useRef } from "react";
import { MdDone, MdDoneAll } from "react-icons/md";
import {
@@ -44,6 +46,16 @@ export default function ChatMessageListComponent({ messages }) {
{MessageRender(messages[index])}
{StatusRender(messages[index].status)}
</div>
{messages[index].isoutbound && (
<div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", {
by: messages[index].userid,
time: moment(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a"
),
})}
</div>
)}
</div>
)}
</CellMeasurer>
@@ -72,15 +84,19 @@ export default function ChatMessageListComponent({ messages }) {
}
const MessageRender = (message) => {
if (message.image) {
return (
<a href={message.image_path} target="__blank">
<img alt="Received" className="message-img" src={message.image_path} />
</a>
);
} else {
return <span>{message.text}</span>;
}
return (
<div>
{message.image_path &&
message.image_path.map((i, idx) => (
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
<a href={i} target="__blank">
<img alt="Received" className="message-img" src={i} />
</a>
</div>
))}
<div>{message.text}</div>
</div>
);
};
const StatusRender = (status) => {

View File

@@ -34,9 +34,10 @@
//display: inline-block;
.message-img {
max-width: 3rem;
max-height: 3rem;
max-width: 10rem;
max-height: 10rem;
object-fit: contain;
margin: 0.2rem;
}
}

View File

@@ -5,7 +5,9 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneFormItem from "../form-items-formatted/phone-form-item.component";
import PhoneFormItem, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -26,7 +28,14 @@ export function ChatNewConversation({ openChatByPhone }) {
const popContent = (
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item label={t("messaging.labels.phonenumber")} name="phoneNumber">
<Form.Item
label={t("messaging.labels.phonenumber")}
name="phoneNumber"
rules={[
({ getFieldValue }) =>
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
]}
>
<PhoneFormItem />
</Form.Item>
<Button type="primary" htmlType="submit">

View File

@@ -1,6 +1,6 @@
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
import { Input, Spin } from "antd";
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -14,6 +14,7 @@ import {
selectMessage,
} from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
const mapStateToProps = createStructuredSelector({
@@ -36,6 +37,7 @@ function ChatSendMessageComponent({
setMessage,
}) {
const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]);
useEffect(() => {
inputArea.current.focus();
}, [isSending, setMessage]);
@@ -43,36 +45,55 @@ function ChatSendMessageComponent({
const { t } = useTranslation();
const handleEnter = () => {
if (message === "" || !message) return;
logImEXEvent("messaging_send_message");
sendMessage({
to: conversation.phone_num,
body: message,
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
});
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if (selectedImages < 11) {
sendMessage({
to: conversation.phone_num,
body: message,
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
});
setSelectedMedia(
selectedMedia.map((i) => {
return { ...i, isSelected: false };
})
);
}
};
return (
<div className="imex-flex-row">
<div className="imex-flex-row" style={{ width: "100%" }}>
<ChatPresetsComponent className="imex-flex-row__margin" />
<Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow"
allowClear
autoFocus
ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={isSending}
placeholder={t("messaging.labels.typeamessage")}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
if (!!!event.shiftKey) handleEnter();
}}
<ChatMediaSelector
conversation={conversation}
selectedMedia={selectedMedia}
setSelectedMedia={setSelectedMedia}
/>
<span style={{ flex: 1 }}>
<Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow"
allowClear
autoFocus
ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={isSending}
placeholder={t("messaging.labels.typeamessage")}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
if (!!!event.shiftKey) handleEnter();
}}
/>
</span>
<SendOutlined
className="imex-flex-row__margin"
disabled={message === "" || !message}
onClick={handleEnter}
/>
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
<Spin
style={{ display: `${isSending ? "" : "none"}` }}
indicator={

View File

@@ -1,5 +1,5 @@
import { PlusOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Tag } from "antd";
import _ from "lodash";
import React, { useState } from "react";

View File

@@ -3,11 +3,12 @@ import Rate from "./rate/rate.component";
import Slider from "./slider/slider.component";
import Text from "./text/text.component";
import Textarea from "./textarea/textarea.component";
export default {
const e = {
checkbox: CheckboxFormItem,
slider: Slider,
text: Text,
textarea: Textarea,
rate: Rate,
};
export default e;

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
@@ -14,6 +14,7 @@ export default function ContractCarsContainer({ selectedCarState, form }) {
form.setFieldsValue({
kmstart: record.mileage,
dailyrate: record.dailycost,
fuelout: record.fuel,
});
};

View File

@@ -1,7 +1,15 @@
import React, { useState } from "react";
import { Button, notification, Popover, Radio, Form, InputNumber } from "antd";
import {
Button,
notification,
Popover,
Radio,
Form,
InputNumber,
Space,
} from "antd";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/client";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import moment from "moment";
import { connect } from "react-redux";
@@ -11,6 +19,8 @@ import {
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { useHistory } from "react-router-dom";
import axios from "axios";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
@@ -20,7 +30,12 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
export function ContractConvertToRo({
bodyshop,
currentUser,
contract,
disabled,
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
@@ -46,6 +61,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
part_type: "CCDR",
tax_part: true,
mod_lb_hrs: 0,
db_ref: "io-ccdr",
// mod_lbr_ty: "PAL",
},
];
@@ -63,6 +79,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
part_type: "CCM",
part_qty: mileageDiff,
tax_part: true,
db_ref: "io-ccm",
mod_lb_hrs: 0,
});
}
@@ -78,6 +95,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
part_qty: values.refuelqty,
part_type: "CCF",
tax_part: true,
db_ref: "io-ccf",
mod_lb_hrs: 0,
});
}
@@ -92,6 +110,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
part_qty: 1,
part_type: "CCC",
tax_part: true,
db_ref: "io-ccc",
mod_lb_hrs: 0,
});
}
@@ -107,6 +126,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
part_type: "CCD",
part_qty: 1,
tax_part: true,
db_ref: "io-ccd",
mod_lb_hrs: 0,
});
}
@@ -119,8 +139,9 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
clm_no: `${contract.job.clm_no}-CC`,
clm_total: 1234, //TODO
ins_co_nm: "CC",
converted: true,
clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
ownr_fn: contract.job.owner.ownr_fn,
ownr_ln: contract.job.owner.ownr_ln,
ownr_co_nm: contract.job.owner.ownr_co_nm,
@@ -145,13 +166,69 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
data: billingLines,
},
parts_tax_rates: {
PAA: {
prt_type: "PAA",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAC: {
prt_type: "PAC",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAL: {
prt_type: "PAL",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAM: {
prt_type: "PAM",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAN: {
prt_type: "PAN",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAR: {
prt_type: "PAR",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
PAS: {
prt_type: "PAS",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
CCDR: {
prt_type: "CCDR",
prt_discp: 0,
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: 0.07,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
CCF: {
prt_type: "CCF",
@@ -159,7 +236,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: 0.07,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
CCM: {
prt_type: "CCM",
@@ -167,7 +244,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: 0.07,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
CCC: {
prt_type: "CCC",
@@ -175,7 +252,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: 0.07,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
CCD: {
prt_type: "CCD",
@@ -183,10 +260,22 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
prt_mktyp: false,
prt_mkupp: 0,
prt_tax_in: true,
prt_tax_rt: 0.07,
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
},
},
};
//Calcualte the new job totals.
const newTotals = (
await axios.post("/job/totals", {
job: { ...newJob, joblines: billingLines },
})
).data;
newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
newJob.job_totals = newTotals;
const result = await insertJob({
variables: { job: [newJob] },
// refetchQueries: ["GET_JOB_BY_PK"],
@@ -244,12 +333,14 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
>
<InputNumber precision={0} min={0} />
</Form.Item>
<Button type="primary" htmlType="submit">
{t("contracts.actions.convertoro")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.close")}
</Button>
<Space>
<Button type="primary" htmlType="submit" loading={loading}>
{t("contracts.actions.convertoro")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.close")}
</Button>
</Space>
</Form>
</div>
);
@@ -257,7 +348,11 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
return (
<div>
<Popover content={popContent} visible={visible}>
<Button onClick={() => setVisible(true)} loading={loading}>
<Button
onClick={() => setVisible(true)}
loading={loading}
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
>
{t("contracts.actions.convertoro")}
</Button>
</Popover>

View File

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

View File

@@ -2,13 +2,20 @@ import { Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
export default function ContractFormComponent({ form, create = false }) {
import InputPhone, {
PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ContractFormJobPrefill from "./contract-form-job-prefill.component";
export default function ContractFormComponent({
form,
create = false,
selectedJobState,
}) {
const { t } = useTranslation();
return (
<div>
@@ -46,12 +53,6 @@ export default function ContractFormComponent({ form, create = false }) {
<Form.Item
label={t("contracts.fields.scheduledreturn")}
name="scheduledreturn"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
@@ -84,6 +85,33 @@ export default function ContractFormComponent({ form, create = false }) {
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item
label={t("contracts.fields.fuelout")}
name="fuelout"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<CourtesyCarFuelSlider />
</Form.Item>
{create ? null : (
<Form.Item label={t("contracts.fields.fuelin")} name="fuelin">
<CourtesyCarFuelSlider />
</Form.Item>
)}
</LayoutFormRow>
{selectedJobState && (
<div>
<ContractFormJobPrefill
jobId={selectedJobState && selectedJobState[0]}
form={form}
/>
</div>
)}
<LayoutFormRow>
<Form.Item
label={t("contracts.fields.driver_dlnumber")}
@@ -163,40 +191,16 @@ export default function ContractFormComponent({ form, create = false }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_city")}
name="driver_city"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Form.Item label={t("contracts.fields.driver_city")} name="driver_city">
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_state")}
name="driver_state"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_zip")}
name="driver_zip"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Form.Item label={t("contracts.fields.driver_zip")} name="driver_zip">
<Input />
</Form.Item>
<Form.Item
@@ -207,62 +211,17 @@ export default function ContractFormComponent({ form, create = false }) {
required: true,
message: t("general.validation.required"),
},
({ getFieldValue }) =>
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
]}
>
<InputPhone />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dob")}
name="driver_dob"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item
label={t("contracts.fields.cc_num")}
name="cc_num"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_expiry")}
name="cc_expiry"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_cardholder")}
name="cc_cardholder"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
<InputNumber precision={2} />

View File

@@ -23,17 +23,13 @@ export default function ContractsJobsComponent({
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: (a, b) =>
alphaSort(
a.ro_number ? a.ro_number : "EST-" + a.est_number,
b.ro_number ? b.ro_number : "EST-" + b.est_number
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<span>
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
{record.ro_number ? record.ro_number : t("general.labels.na")}
</span>
),
},
@@ -135,10 +131,6 @@ export default function ContractsJobsComponent({
? data
: data.filter(
(j) =>
(j.est_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ro_number || "")
.toString()
.toLowerCase()

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";

View File

@@ -62,9 +62,11 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
//sortOrder:
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={`/manage/courtesycars/${record.courtesycar.id}`}
>{`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`}</Link>
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
record.courtesycar.year
} ${record.courtesycar.make} ${record.courtesycar.model} ${
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
}`}</Link>
),
},
{

View File

@@ -2,7 +2,7 @@ import { Form, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from '../form-date-picker/form-date-picker.component';
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
@@ -15,8 +15,8 @@ export default function CourtesyCarReturnModalComponent() {
rules={[
{
required: true,
message: t("general.validation.required")
}
message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
@@ -27,20 +27,20 @@ export default function CourtesyCarReturnModalComponent() {
rules={[
{
required: true,
message: t("general.validation.required")
}
message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fuel")}
name={["courtesycar", "data", "fuel"]}
label={t("contracts.fields.fuelin")}
name={"fuelin"}
rules={[
{
required: true,
message: t("general.validation.required")
}
message: t("general.validation.required"),
},
]}
>
<CourtesyCarFuelSlider />

View File

@@ -9,7 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
import moment from "moment";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/react-hooks";
import { useMutation } from "@apollo/client";
const mapStateToProps = createStructuredSelector({
courtesyCarReturnModal: selectCourtesyCarReturn,
@@ -39,11 +39,12 @@ export function BillEnterModalContainer({
kmend: values.kmend,
actualreturn: values.actualreturn,
status: "contracts.status.returned",
fuelin: values.fuelin,
},
courtesycarid: context.courtesyCarId,
courtesycar: {
status: "courtesycars.status.in",
fuel: values.fuel,
fuel: values.fuelin,
mileage: values.kmend,
},
},

View File

@@ -3,10 +3,7 @@ import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = (
{ value = "courtesycars.status.in", onChange },
ref
) => {
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();

View File

@@ -75,6 +75,14 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
key: "plate",
sorter: (a, b) => alphaSort(a.plate, b.plate),
sortOrder:
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
},
{
title: t("courtesycars.labels.outwith"),
dataIndex: "outwith",

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import { Form } from "antd";
import queryString from "query-string";
import React, { useEffect } from "react";

View File

@@ -31,21 +31,9 @@ export default function CsiResponseListPaginated({
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
),
},
{
title: t("jobs.fields.est_number"),
dataIndex: "est_number",
key: "est_number",
width: "8%",
sorter: (a, b) => a.job.est_number - b.job.est_number,
sortOrder: sortcolumn === "est_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.est_number}
{record.job.ro_number || t("general.labels.na")}
</Link>
),
},

View File

@@ -1,7 +1,7 @@
// import Icon from "@ant-design/icons";
// import { Button, Dropdown, Menu, notification } from "antd";
// import React, { useState } from "react";
// import { useMutation, useQuery } from "react-apollo";
// import { useMutation, useQuery } from "@apollo/client";
// import { Responsive, WidthProvider } from "react-grid-layout";
// import { useTranslation } from "react-i18next";
// import { MdClose } from "react-icons/md";

View File

@@ -1,5 +1,5 @@
import { UploadOutlined } from "@ant-design/icons";
import { Button, Upload } from "antd";
import { Upload } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -15,6 +15,7 @@ const mapStateToProps = createStructuredSelector({
});
export function DocumentsUploadComponent({
children,
currentUser,
bodyshop,
jobId,
@@ -23,7 +24,7 @@ export function DocumentsUploadComponent({
callbackAfterUpload,
}) {
return (
<Upload
<Upload.Dragger
multiple={true}
customRequest={(ev) =>
handleUpload(ev, {
@@ -36,12 +37,24 @@ export function DocumentsUploadComponent({
})
}
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
showUploadList={false}
// showUploadList={false}
>
<Button type="primary">
<UploadOutlined />
</Button>
</Upload>
{
// <Button type="primary">
// <UploadOutlined />
// </Button>
}
{children || (
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
)}
</Upload.Dragger>
);
}
export default connect(mapStateToProps, null)(DocumentsUploadComponent);

View File

@@ -1,9 +1,10 @@
import { notification } from "antd";
import axios from "axios";
import i18n from "i18next";
import client, { axiosAuthInterceptorId } from "../../utils/CleanAxios";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
import client from "../../utils/GraphQLClient";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
//Required to prevent headers from getting set and rejected from Cloudinary.
@@ -19,8 +20,10 @@ export const handleUpload = (ev, context) => {
const { bodyshop, jobId } = context;
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
let extension = ev.file.name.split(".").pop();
uploadToCloudinary(
key,
extension,
ev.file.type,
ev.file,
onError,
@@ -32,6 +35,7 @@ export const handleUpload = (ev, context) => {
export const uploadToCloudinary = async (
key,
extension,
fileType,
file,
onError,
@@ -50,12 +54,16 @@ export const uploadToCloudinary = async (
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
//Get the signed url.
console.log("fileType", fileType);
const upload_preset = fileType.startsWith("video")
? "incoming_upload_video"
: "incoming_upload";
const signedURLResponse = await axios.post("/media/sign", {
public_id: public_id,
tags: tags,
timestamp: timestamp,
upload_preset: "incoming_upload",
upload_preset: upload_preset,
});
if (signedURLResponse.status !== 200) {
@@ -79,8 +87,8 @@ export const uploadToCloudinary = async (
};
const formData = new FormData();
formData.append("file", file);
console.log("Applying lower quality transforms.");
formData.append("upload_preset", "incoming_upload");
formData.append("upload_preset", upload_preset);
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
formData.append("public_id", public_id);
@@ -88,9 +96,10 @@ export const uploadToCloudinary = async (
formData.append("timestamp", timestamp);
formData.append("signature", signature);
//Upload request to Cloudinary
const cloudinaryUploadResponse = await cleanAxios.post(
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType(
fileType
)}/upload`,
formData,
{
...options,
@@ -118,11 +127,13 @@ export const uploadToCloudinary = async (
variables: {
docInput: [
{
jobid: jobId,
...(jobId ? { jobid: jobId } : {}),
...(billId ? { billid: billId } : {}),
uploaded_by: uploaded_by,
key: key,
billid: billId,
type: fileType,
extension: extension,
bodyshopid: bodyshop.id,
},
],
},
@@ -145,9 +156,19 @@ export const uploadToCloudinary = async (
if (!!onError) onError(JSON.stringify(documentInsert.errors));
notification["error"]({
message: i18n.t("documents.errors.insert", {
message: JSON.stringify(JSON.stringify(documentInsert.errors)),
message: JSON.stringify(documentInsert.errors),
}),
});
return;
}
};
export function DetermineFileType(filetype) {
if (!filetype) return "auto";
else if (filetype.startsWith("image")) return "image";
else if (filetype.startsWith("video")) return "video";
else if (filetype.startsWith("application/pdf")) return "image";
else if (filetype.startsWith("application")) return "raw";
return "auto";
}

View File

@@ -1,7 +1,8 @@
import { UploadOutlined } from "@ant-design/icons";
import { Editor } from "@tinymce/tinymce-react";
import { Button, Input, Upload, Select } from "antd";
import { Button, Card, Input, Select, Upload } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function EmailOverlayComponent({
messageOptions,
@@ -11,6 +12,7 @@ export default function EmailOverlayComponent({
handleUpload,
handleFileRemove,
}) {
const { t } = useTranslation();
return (
<div>
To:
@@ -18,22 +20,28 @@ export default function EmailOverlayComponent({
name="to"
mode="tags"
value={messageOptions.to}
style={{ width: "100%" }}
//style={{ width: "100%" }}
onChange={handleToChange}
tokenSeparators={[",", ";"]}
/>
CC:
<Input
<Select
value={messageOptions.cc}
onChange={handleConfigChange}
mode="tags"
onChange={(value) => handleConfigChange("cc", value)}
name="cc"
tokenSeparators={[",", ";"]}
/>
Subject:
<Input
value={messageOptions.subject}
onChange={handleConfigChange}
onChange={(e) => handleConfigChange("subject", e.target.value)}
name="subject"
/>
<div style={{ color: "red" }}>
DEVELOPER NOTE: Any edits made in the editor below will not be sent or
saved due to css inlining issues.
</div>
<Editor
value={messageOptions.html}
apiKey="f3s2mjsd77ya5qvqkee9vgh612cm6h41e85efqakn2d0kknk"
@@ -53,17 +61,20 @@ export default function EmailOverlayComponent({
}}
onEditorChange={handleHtmlChange}
/>
<Upload
fileList={messageOptions.fileList}
beforeUpload={handleUpload}
onRemove={handleFileRemove}
multiple
listType="picture-card"
>
<Button>
<UploadOutlined /> Upload
</Button>
</Upload>
<Card title={t("emails.labels.attachments")}>
<Upload
fileList={messageOptions.fileList}
beforeUpload={handleUpload}
onRemove={handleFileRemove}
multiple
listType="picture-card"
style={{ width: "100%" }}
>
<Button>
<UploadOutlined /> Upload
</Button>
</Upload>
</Card>
</div>
);
}

View File

@@ -36,6 +36,7 @@ export function EmailOverlayContainer({
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false);
const [rawHtml, setRawHtml] = useState("");
const defaultEmailFrom = {
from: {
name: bodyshop.shopname || EmailSettings.fromNameDefault,
@@ -65,7 +66,11 @@ export function EmailOverlayContainer({
setSending(true);
try {
await axios.post("/sendemail", { ...messageOptions, attachments });
await axios.post("/sendemail", {
...messageOptions,
html: rawHtml,
attachments,
});
notification["success"]({ message: t("emails.successes.sent") });
toggleEmailOverlayVisible();
} catch (error) {
@@ -77,8 +82,7 @@ export function EmailOverlayContainer({
setSending(false);
};
const handleConfigChange = (event) => {
const { name, value } = event.target;
const handleConfigChange = (name, value) => {
setMessageOptions({ ...messageOptions, [name]: value });
};
const handleHtmlChange = (text) => {
@@ -109,11 +113,19 @@ export function EmailOverlayContainer({
const render = async () => {
logImEXEvent("email_render_template", { template: emailConfig.template });
setLoading(true);
let html = await RenderTemplate(emailConfig.template, bodyshop);
let html = await RenderTemplate(emailConfig.template, bodyshop, true);
const response = await axios.post("/render/inlinecss", {
html: html,
url: `${window.location.protocol}://${window.location.host}/`,
});
setRawHtml(response.data);
console.log("response", response);
setMessageOptions({
...emailConfig.messageOptions,
...defaultEmailFrom,
html: html,
html: response.data,
fileList: [],
});
setLoading(false);

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

View File

@@ -1,4 +1,4 @@
import { Select, Tag } from "antd";
import { Select, Space, Tag } from "antd";
import React, { forwardRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
@@ -35,18 +35,16 @@ const EmployeeSearchSelect = (
key={o.id}
value={o.id}
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
discount={o.discount}
>
<div style={{ display: "flex" }}>
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="blue">{o.cost_center}</Tag>
<Tag color="green">
{o.flat_rate
? t("timetickets.labels.flat_rate")
: t("timetickets.labels.straight_time")}
</Tag>
</div>
</Space>
</Option>
))
: null}

View File

@@ -1,9 +1,8 @@
import { withApollo } from "@apollo/client/react/hoc";
import React, { Component } from "react";
import { withApollo } from "react-apollo";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
import { UPDATE_FCM_TOKEN } from "../../graphql/user.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
@@ -15,17 +14,17 @@ const mapDispatchToProps = (dispatch) => ({
class FcmNotificationComponent extends Component {
async componentDidMount() {
const { client, currentUser } = this.props;
//const { client, currentUser } = this.props;
if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
messaging
.requestPermission()
.then(async function () {
const token = await messaging.getToken();
client.mutate({
mutation: UPDATE_FCM_TOKEN,
variables: { authEmail: currentUser.email, token: { [token]: true } },
});
// const token = await messaging.getToken();
// client.mutate({
// mutation: UPDATE_FCM_TOKEN,
// variables: { authEmail: currentUser.email, token: { [token]: true } },
// });
})
.catch(function (err) {
console.log("Unable to get permission to notify.", err);
@@ -44,22 +43,19 @@ export default connect(
)(withApollo(FcmNotificationComponent));
//Firebase Service Worker Register
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/firebase-messaging-sw.js")
.then(function (registration) {
console.log(
"[FCM] Registration successful, scope is:",
registration.scope
);
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("Handler for Navigator Service Worker.", event);
});
})
.catch(function (err) {
console.log(
"[FCM] Service worker registration failed, error:",
err
);
});
}
// if ("serviceWorker" in navigator) {
// navigator.serviceWorker
// .register("/firebase-messaging-sw.js")
// .then(function (registration) {
// console.log(
// "[FCM] Registration successful, scope is:",
// registration.scope
// );
// navigator.serviceWorker.addEventListener("message", (event) => {
// console.log("Handler for Navigator Service Worker.", event);
// });
// })
// .catch(function (err) {
// console.log("[FCM] Service worker registration failed, error:", err);
// });
// }

View File

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

View File

@@ -6,7 +6,7 @@ import { TimePicker } from "antd";
import moment from "moment";
//To be used as a form element only.
const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
const DateTimePicker = ({ value, onChange, onBlur, id, ...restProps }, ref) => {
// const handleChange = (newDate) => {
// if (value !== newDate && onChange) {
// onChange(newDate);
@@ -14,7 +14,7 @@ const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
// };
return (
<div>
<div id={id}>
<FormDatePicker
{...restProps}
value={value}

View File

@@ -12,7 +12,6 @@ const FormInputNUmberCalculator = (
const ref = useRef(null);
const handleKeyDown = (e) => {
console.log("e :>> ", e.currentTarget.value);
const { key } = e;
let action;
switch (key) {

View File

@@ -1,7 +1,50 @@
import i18n from "i18next";
import parsePhoneNumber from "libphonenumber-js";
import React, { forwardRef } from "react";
import NumberFormat from "react-number-format";
import PhoneInput from "react-phone-input-2";
import "react-phone-input-2/lib/high-res.css";
import "./phone-form-item.styles.scss";
function FormItemPhone(props, ref) {
return <NumberFormat {...props} ref={ref} type="tel" format="###-###-####" />;
return (
<PhoneInput
country="ca"
onlyCountries={["ca", "us"]}
ref={ref}
className="ant-input"
{...props}
/>
);
}
export default forwardRef(FormItemPhone);
export const PhoneItemFormatterValidation = (getFieldValue, name) => ({
async validator(rule, value) {
if (!getFieldValue(name)) {
return Promise.resolve();
} else {
const p = parsePhoneNumber(getFieldValue(name), "CA");
if (p) {
return Promise.resolve();
} else {
return Promise.reject(i18n.t("general.validation.invalidphone"));
}
}
// PhoneInput({
// value: getFieldValue(name),
// isValid: async (value, country) => {
// console.log("value", value);
// if (value.match(/12345/)) {
// } else if (value.match(/1234/)) {
// return false;
// } else {
// return Promise.resolve();
// }
// },
// });
},
});

View File

@@ -0,0 +1,6 @@
.react-tel-input .form-control {
width: 100% !important;
height: unset !important;
border-radius: unset !important;
line-height: unset !important;
}

View File

@@ -1,5 +1,5 @@
import { DownOutlined, UpOutlined } from "@ant-design/icons";
import React from "react";
import { UpOutlined, DownOutlined } from "@ant-design/icons";
export default function FormListMoveArrows({ move, index, total }) {
const upDisabled = index === 0;
const downDisabled = index === total - 1;
@@ -9,7 +9,7 @@ export default function FormListMoveArrows({ move, index, total }) {
};
const handleDown = () => {
move(index, index - 1);
move(index, index + 1);
};
return (

View File

@@ -1,12 +1,13 @@
import { useLazyQuery } from "@apollo/react-hooks";
import { useLazyQuery } from "@apollo/client";
import { AutoComplete, Input } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import AlertComponent from "../alert/alert.component";
export default function GlobalSearch() {
const { t } = useTranslation();
@@ -14,27 +15,18 @@ export default function GlobalSearch() {
GLOBAL_SEARCH_QUERY
);
const handleSearch = (searchTerm) => {
logImEXEvent("global_search", { term: searchTerm });
const executeSearch = (v) => {
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
if (searchTerm.length > 0)
callSearch({ variables: { search: searchTerm } });
const handleSearch = (value) => {
if (value && value !== "")
debouncedExecuteSearch({ variables: { search: value } });
};
const renderTitle = (title) => {
return (
<span>
{title}
<a
style={{ float: "right" }}
href="https://www.google.com/search?q=antd"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
</span>
);
return <span>{title}</span>;
};
const options = data
@@ -48,11 +40,7 @@ export default function GlobalSearch() {
<Link to={`/manage/jobs/${job.id}`}>
<div className="imex-flex-row">
<span className="imex-flex-row__margin-large">
<strong>
{job.ro_number
? `${job.ro_number || ""} / ${job.est_number || ""}`
: `${job.est_number || ""}`}
</strong>
<strong>{job.ro_number || t("general.labels.na")}</strong>
</span>
<span className="imex-flex-row__margin-large">{`${
@@ -154,14 +142,13 @@ export default function GlobalSearch() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<AutoComplete
dropdownMatchSelectWidth={false}
style={{ width: 200 }}
options={options}
>
<Input.Search loading={loading} onSearch={handleSearch} />
</AutoComplete>
</div>
<AutoComplete
dropdownMatchSelectWidth={false}
style={{ flex: 2 }}
options={options}
onSearch={handleSearch}
>
<Input.Search loading={loading} />
</AutoComplete>
);
}

View File

@@ -10,9 +10,9 @@ import Icon, {
LineChartOutlined,
ScheduleOutlined,
TeamOutlined,
ToolFilled,
UnorderedListOutlined,
UserOutlined,
ToolFilled,
} from "@ant-design/icons";
import { Avatar, Menu } from "antd";
import React from "react";
@@ -35,8 +35,6 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import "./header.styles.scss";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
recentItems: selectRecentItems,
@@ -50,6 +48,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
setReportCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
signOutStart: () => dispatch(signOutStart()),
});
@@ -61,285 +61,313 @@ function Header({
setBillEnterContext,
setTimeTicketContext,
setPaymentContext,
setReportCenterContext,
recentItems,
}) {
const { t } = useTranslation();
return (
<Menu
mode="horizontal"
theme="dark"
// style={{ backgroundColor: "#eee" }}
className="header-main-menu"
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
>
<Menu.Item key="home">
<Link to="/manage">
<HomeFilled />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon component={FaCalendarAlt} />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon component={FaCarCrash} />
<span>{t("menus.header.jobs")}</span>
</span>
}
<div style={{ display: "flex", alignItems: "center" }}>
<Menu
mode="horizontal"
theme="dark"
style={{ flex: 5 }}
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
subMenuCloseDelay={0.3}
>
<Menu.Item key="activejobs">
<FileFilled />
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="parts-queue">
<Link to="/manage/partsqueue">
<ToolFilled /> {t("menus.header.parts-queue")}
<Menu.Item key="home">
<Link to="/manage">
<HomeFilled />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.Item key="availablejobs">
<Link to="/manage/available">
<ImportOutlined /> {t("menus.header.availablejobs")}
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon component={FaCalendarAlt} />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="alljobs">
<UnorderedListOutlined />
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="productionlist">
<Link to="/manage/production/list">
<ScheduleOutlined />
{t("menus.header.productionlist")}
</Link>
</Menu.Item>
<Menu.Item key="productionboard">
<Link to="/manage/production/board">
<Icon component={BsKanban} />
{t("menus.header.productionboard")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="scoreboard">
<LineChartOutlined />
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<UserOutlined />
<span>{t("menus.header.customers")}</span>
</span>
}
>
<Menu.Item key="owners">
<Link to="/manage/owners">
<TeamOutlined />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<CarFilled />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<CarFilled />
<span>{t("menus.header.courtesycars")}</span>
</span>
}
>
<Menu.Item key="courtesycarsall">
<Link to="/manage/courtesycars">
<CarFilled />
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item>
<Menu.Item key="contracts">
<Link to="/manage/courtesycars/contracts">
<FileFilled />
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item>
<Menu.Item key="newcontract">
<Link to="/manage/courtesycars/contracts/new">
<FileAddFilled />
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<DollarCircleFilled />
<span>{t("menus.header.accounting")}</span>
</span>
}
>
<Menu.Item key="bills">
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
</Menu.Item>
<Menu.Item
key="enterbills"
onClick={() => {
setBillEnterContext({
actions: {},
context: {},
});
}}
>
<Icon component={FaFileInvoiceDollar} />
{t("menus.header.enterbills")}
</Menu.Item>
<Menu.Divider />
<Menu.Item key="allpayments">
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
<Menu.Item
key="enterpayments"
onClick={() => {
setPaymentContext({
actions: {},
context: {},
});
}}
>
<Icon component={FaCreditCard} />
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Divider />
<Menu.Item key="timetickets">
<Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
onClick={() => {
setTimeTicketContext({
actions: {},
context: {},
});
}}
>
{t("menus.header.entertimeticket")}
</Menu.Item>
<Menu.Divider />
<Menu.SubMenu title={t("menus.header.export")}>
<Menu.Item key="receivables">
<Link to="/manage/accounting/receivables">
{t("menus.header.accounting-receivables")}
</Link>
</Menu.Item>
<Menu.Item key="payables">
<Link to="/manage/accounting/payables">
{t("menus.header.accounting-payables")}
</Link>
</Menu.Item>
<Menu.Item key="payments">
<Link to="/manage/accounting/payments">
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="shop-templates">
<Link to="/manage/shop/templates">
{t("menus.header.shop_templates")}
</Link>
</Menu.Item>
<Menu.Item key="shop-vendors">
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
<Menu.Item key="shop-csi">
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item>
<GlobalSearch />
</Menu.Item>
<Menu.SubMenu title={<ClockCircleFilled />}>
{recentItems.map((i, idx) => (
<Menu.Item key={idx}>
<Link to={i.url}>{i.label}</Link>
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={
<div>
{currentUser.photoURL ? (
<Avatar
src={currentUser.photoURL}
style={{
margin: "10px",
}}
/>
) : (
<Avatar
style={{
backgroundColor: "#87d068",
margin: "10px",
}}
icon={<UserOutlined />}
/>
)}
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item danger onClick={() => signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item key="shiftclock">
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
</Menu.Item>
<Menu.Item key="profile">
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<GlobalOutlined />
<span>{t("menus.currentuser.languageselector")}</span>
<Icon component={FaCarCrash} />
<span>{t("menus.header.jobs")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en-US">
{t("general.languages.english")}
<Menu.Item key="activejobs">
<FileFilled />
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr-CA">
{t("general.languages.french")}
<Menu.Item key="parts-queue">
<Link to="/manage/partsqueue">
<ToolFilled /> {t("menus.header.parts-queue")}
</Link>
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es-MX">
{t("general.languages.spanish")}
<Menu.Item key="availablejobs">
<Link to="/manage/available">
<ImportOutlined /> {t("menus.header.availablejobs")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="alljobs">
<UnorderedListOutlined />
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="productionlist">
<Link to="/manage/production/list">
<ScheduleOutlined />
{t("menus.header.productionlist")}
</Link>
</Menu.Item>
<Menu.Item key="productionboard">
<Link to="/manage/production/board">
<Icon component={BsKanban} />
{t("menus.header.productionboard")}
</Link>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="scoreboard">
<LineChartOutlined />
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
<Menu.SubMenu
title={
<span>
<UserOutlined />
<span>{t("menus.header.customers")}</span>
</span>
}
>
<Menu.Item key="owners">
<Link to="/manage/owners">
<TeamOutlined />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<CarFilled />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<CarFilled />
<span>{t("menus.header.courtesycars")}</span>
</span>
}
>
<Menu.Item key="courtesycarsall">
<Link to="/manage/courtesycars">
<CarFilled />
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item>
<Menu.Item key="contracts">
<Link to="/manage/courtesycars/contracts">
<FileFilled />
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item>
<Menu.Item key="newcontract">
<Link to="/manage/courtesycars/contracts/new">
<FileAddFilled />
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<DollarCircleFilled />
<span>{t("menus.header.accounting")}</span>
</span>
}
>
<Menu.Item key="bills">
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
</Menu.Item>
<Menu.Item
key="enterbills"
onClick={() => {
setBillEnterContext({
actions: {},
context: {},
});
}}
>
<Icon component={FaFileInvoiceDollar} />
{t("menus.header.enterbills")}
</Menu.Item>
<Menu.Divider />
<Menu.Item key="allpayments">
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
<Menu.Item
key="enterpayments"
onClick={() => {
setPaymentContext({
actions: {},
context: null,
});
}}
>
<Icon component={FaCreditCard} />
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Divider />
<Menu.Item key="timetickets">
<Link to="/manage/timetickets">
{t("menus.header.timetickets")}
</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
onClick={() => {
setTimeTicketContext({
actions: {},
context: {},
});
}}
>
{t("menus.header.entertimeticket")}
</Menu.Item>
<Menu.Divider />
<Menu.SubMenu title={t("menus.header.export")}>
<Menu.Item key="receivables">
<Link to="/manage/accounting/receivables">
{t("menus.header.accounting-receivables")}
</Link>
</Menu.Item>
<Menu.Item key="payables">
<Link to="/manage/accounting/payables">
{t("menus.header.accounting-payables")}
</Link>
</Menu.Item>
<Menu.Item key="payments">
<Link to="/manage/accounting/payments">
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="temporarydocs">
<Link to="/manage/temporarydocs">
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item>
{
// <Menu.Item key="shop-templates">
// <Link to="/manage/shop/templates">
// {t("menus.header.shop_templates")}
// </Link>
// </Menu.Item>
}
<Menu.Item
key="reportcenter"
onClick={() => {
setReportCenterContext({
actions: {},
context: {},
});
}}
>
{t("menus.header.reportcenter")}
</Menu.Item>
<Menu.Item key="shop-vendors">
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
<Menu.Item key="shop-csi">
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu>
<GlobalSearch />
<Menu
mode="horizontal"
theme="dark"
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
>
<Menu.SubMenu
title={
<div>
{currentUser.photoURL ? (
<Avatar
src={currentUser.photoURL}
style={{
margin: "10px",
}}
/>
) : (
<Avatar
style={{
backgroundColor: "#87d068",
margin: "10px",
}}
icon={<UserOutlined />}
/>
)}
{currentUser.displayName ||
currentUser.email ||
t("general.labels.unknown")}
</div>
}
>
<Menu.Item danger onClick={() => signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item key="shiftclock">
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
</Menu.Item>
<Menu.Item key="profile">
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<GlobalOutlined />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en-US">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr-CA">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es-MX">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu title={<ClockCircleFilled />}>
{recentItems.map((i, idx) => (
<Menu.Item key={idx}>
<Link to={i.url}>{i.label}</Link>
</Menu.Item>
))}
</Menu.SubMenu>
</Menu>
</div>
);
}

View File

@@ -1,9 +0,0 @@
.header-shop-logo {
background-size: cover;
max-width: 100%;
max-height: 3.5rem;
}
.header-main-menu {
//width: 95vw;
//float: left;
}

View File

@@ -32,7 +32,7 @@ export default function HelpRescue() {
>
<span>
Enter your six-digit code, then click the Start Download button
below{" "}
below
</span>
<input type="text" name="Code" />
<br />

View File

@@ -25,5 +25,5 @@ export default function JiraSupportComponent() {
};
}, []);
return <div>JIra</div>;
return <div></div>;
}

View File

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

View File

@@ -17,30 +17,49 @@ export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
const totals = jobTotals;
let billTotals = Dinero({ amount: 0 });
let billTotals = Dinero();
let billCms = Dinero();
let lbrAdjustments = Dinero();
bills.forEach((i) =>
i.billlines.forEach((il) => {
billTotals = billTotals.add(
Dinero({
amount: Math.round(
(il.actual_cost || 0) * (i.is_credit_memo ? -1 : 1) * 100
),
}).multiply(il.quantity)
);
if (!i.is_credit_memo) {
billTotals = billTotals.add(
Dinero({
amount: Math.round((il.actual_price || 0) * 100),
}).multiply(il.quantity)
);
} else {
billCms = billCms.add(
Dinero({
amount: Math.round((il.actual_price || 0) * -100),
}).multiply(il.quantity)
);
}
if (il.deductedfromlbr) {
console.log(i, "Deducting from labor.");
lbrAdjustments = lbrAdjustments.add(
Dinero({
amount: Math.round((il.actual_price || 0) * 100),
}).multiply(il.quantity)
);
}
})
);
const discrepancy = Dinero(totals.parts.parts.total).subtract(billTotals);
const totalPartsSublet = Dinero(totals.parts.parts.total).add(
Dinero(totals.parts.sublets.total)
);
const discrepancy = totalPartsSublet.subtract(billTotals);
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.subtract(billCms);
return (
<div className="job-bills-totals-container">
<Statistic
title={t("jobs.labels.partstotal")}
value={Dinero(totals.parts.parts.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.subletstotal")}
value={Dinero(totals.parts.sublets.total).toFormat()}
title={t("jobs.labels.rosaletotal")}
value={totalPartsSublet.toFormat()}
/>
<Statistic
title={t("bills.labels.retailtotal")}
@@ -53,6 +72,28 @@ export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
}}
value={discrepancy.toFormat()}
/>
<Statistic
title={t("bills.labels.dedfromlbr")}
value={lbrAdjustments.toFormat()}
/>
<Statistic
title={t("bills.labels.discrepwithlbradj")}
valueStyle={{
color: discrepWithLbrAdj.getAmount === 0 ? "green" : "red",
}}
value={discrepWithLbrAdj.toFormat()}
/>
<Statistic
title={t("bills.labels.billcmtotal")}
value={billCms.toFormat()}
/>
<Statistic
title={t("bills.labels.discrepwithcms")}
valueStyle={{
color: discrepWithCms.getAmount === 0 ? "green" : "red",
}}
value={discrepWithCms.toFormat()}
/>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { Button, notification } from "antd";
import Axios from "axios";
import React, { useState } from "react";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";

View File

@@ -1,4 +1,4 @@
import { useMutation } from "@apollo/react-hooks";
import { useMutation } from "@apollo/client";
import { Button, Form, notification, Switch } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
@@ -30,6 +30,7 @@ export function JobChecklistForm({
currentUser,
type,
job,
readOnly = false,
}) {
const { t } = useTranslation();
const [intakeJob] = useMutation(UPDATE_JOB);
@@ -57,10 +58,18 @@ export function JobChecklistForm({
...(type === "intake" && {
scheduled_completion: values.scheduled_completion,
}),
...(type === "deliver" && {
actual_completion: values.actual_completion,
}),
[(type === "intake" && "intakechecklist") ||
(type === "deliver" && "deliverchecklist")]: {
...values,
form: formItems.map((fi) => {
return {
...fi,
value: values[fi.name],
};
}),
completed_by: currentUser.email,
completed_at: new Date(),
},
@@ -112,11 +121,21 @@ export function JobChecklistForm({
scheduled_completion: job && job.scheduled_completion,
scheduled_delivery: job && job.scheduled_delivery,
}),
...(type === "deliver" && {
removeFromProduction: true,
actual_completion: job && job.actual_completion,
}),
...formItems
.filter((fi) => fi.value)
.reduce((acc, fi) => {
acc[fi.name] = fi.value;
return acc;
}, {}),
}}
>
{t("checklist.labels.checklist")}
<ConfigFormComponents componentList={formItems} />
<ConfigFormComponents componentList={formItems} readOnly={readOnly} />
{type === "intake" && (
<div>
@@ -124,12 +143,14 @@ export function JobChecklistForm({
name="addToProduction"
valuePropName="checked"
label={t("checklist.labels.addtoproduction")}
disabled={readOnly}
>
<Switch />
</Form.Item>
<Form.Item
name="scheduled_completion"
label={t("jobs.fields.scheduled_completion")}
disabled={readOnly}
rules={[
{
required: true,
@@ -142,6 +163,7 @@ export function JobChecklistForm({
<Form.Item
name="scheduled_delivery"
label={t("jobs.fields.scheduled_delivery")}
disabled={readOnly}
>
<DateTimePicker />
</Form.Item>
@@ -150,8 +172,9 @@ export function JobChecklistForm({
{type === "deliver" && (
<div>
<Form.Item
name="actualCompletion"
name="actual_completion"
label={t("jobs.fields.actual_completion")}
disabled={readOnly}
rules={[
{
required: true,
@@ -165,15 +188,17 @@ export function JobChecklistForm({
name="removeFromProduction"
valuePropName="checked"
label={t("checklist.labels.removefromproduction")}
disabled={readOnly}
>
<Switch />
<Switch defaultChecked={true} />
</Form.Item>
</div>
)}
<Button loading={loading} htmlType="submit">
{t("general.actions.submit")}
</Button>
{!readOnly && (
<Button loading={loading} htmlType="submit">
{t("general.actions.submit")}
</Button>
)}
</Form>
);
}

View File

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

View File

@@ -1,63 +1,77 @@
import React from "react";
import JobIntakeTemplateItem from "../job-checklist-template-item/job-checklist-template-item.component";
import { useParams } from "react-router-dom";
import RenderTemplate, {
displayTemplateInWindow,
} from "../../../../utils/RenderTemplate";
import { Button } from "antd";
import { selectBodyshop } from "../../../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { PrinterFilled } from "@ant-design/icons";
import { Button, List } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { logImEXEvent } from "../../../../firebase/firebase.utils";
import {
GenerateDocument,
GenerateDocuments,
} from "../../../../utils/RenderTemplate";
import { TemplateList } from "../../../../utils/TemplateConstants";
const TemplateListGenerated = TemplateList();
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobIntakeTemplateList({ bodyshop, templates }) {
export default function JobIntakeTemplateList({ templates }) {
const { jobId } = useParams();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const renderTemplate = async (templateKey) => {
setLoading(true);
logImEXEvent("job_checklist_template_render");
const html = await RenderTemplate(
await GenerateDocument(
{
name: templateKey,
variables: { id: jobId },
},
bodyshop
{},
"p"
);
displayTemplateInWindow(html);
setLoading(false);
};
const renderAllTemplates = () => {
logImEXEvent("job_checklist_render_all_templates");
templates.forEach((template) => renderTemplate(template));
const renderAllTemplates = async () => {
logImEXEvent("checklist_render_all_templates");
setLoading(true);
console.log("templates :>> ", templates);
await GenerateDocuments(
templates.map((key) => {
return { name: key, variables: { id: jobId } };
})
);
setLoading(false);
};
return (
<div>
{t("intake.labels.printpack")}
<Button onClick={renderAllTemplates}>
<Button onClick={renderAllTemplates} loading={loading}>
{t("checklist.actions.printall")}
</Button>
{templates.map((template) => (
<JobIntakeTemplateItem
key={template}
templateKey={template}
renderTemplate={renderTemplate}
/>
))}
<List
itemLayout="horizontal"
dataSource={templates}
renderItem={(template) => (
<List.Item
actions={[
<Button
loading={loading}
onClick={() => renderTemplate(template)}
>
<PrinterFilled />
</Button>,
]}
>
<List.Item.Meta
title={
TemplateListGenerated[template] &&
TemplateListGenerated[template].title
}
// description={renderTemplateList(template.templates)}
/>
</List.Item>
)}
/>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobIntakeTemplateList);

View File

@@ -6,11 +6,11 @@ export default function JobIntakeComponent({ checklistConfig, type, job }) {
const { form, templates } = checklistConfig;
return (
<Row>
<Col span={12}>
<Row gutter={[48, 48]}>
<Col span={6}>
<JobChecklistTemplateList templates={templates} type={type} />
</Col>
<Col span={12}>
<Col span={18}>
<JobChecklistForm formItems={form} type={type} job={job} />
</Col>
</Row>

View File

@@ -1,11 +1,14 @@
import { Typography } from "antd";
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
import JobCostingPie from "./job-costing-modal.pie.component";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -15,42 +18,49 @@ const mapDispatchToProps = (dispatch) => ({
export function JobCostingModalComponent({ bodyshop, job }) {
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
const jobLineTotalsByProfitCenter = job.joblines.reduce(
(acc, val) => {
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
const laborAmount = Dinero({
amount: Math.round((job[rateName] || 0) * 100),
}).multiply(val.mod_lb_hrs || 0);
if (!!!acc.labor[laborProfitCenter])
acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
laborAmount
);
const partsProfitCenter = defaultProfits[val.part_type] || "?";
if (!!!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for parts.",
val.part_type
);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1);
if (!!!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
partsAmount
);
return acc;
},
{ parts: {}, labor: {} }
const allProfitCenters = _.union(
bodyshop.md_responsibility_centers.profits.map((p) => p.name),
bodyshop.md_responsibility_centers.costs.map((p) => p.name)
);
// const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
const { t } = useTranslation();
const jobLineTotalsByProfitCenter =
job &&
job.joblines.reduce(
(acc, val) => {
const laborProfitCenter = defaultProfits[val.mod_lbr_ty] || "?";
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
const laborAmount = Dinero({
amount: Math.round((job[rateName] || 0) * 100),
}).multiply(val.mod_lb_hrs || 0);
if (!!!acc.labor[laborProfitCenter])
acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
laborAmount
);
const partsProfitCenter = defaultProfits[val.part_type] || "?";
if (!!!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for parts.",
val.part_type
);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1);
if (!!!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
partsAmount
);
return acc;
},
{ parts: {}, labor: {} }
);
const billTotalsByProfitCenter = job.bills.reduce((bill_acc, bill_val) => {
//At the invoice level.
bill_val.billlines.map((line_val) => {
@@ -66,6 +76,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
return null;
});
return bill_acc;
@@ -82,7 +93,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(ticket_val.actualhrs || 0)
}).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0)
);
return ticket_acc;
@@ -102,8 +113,8 @@ export function JobCostingModalComponent({ bodyshop, job }) {
gppercentFormatted: null,
};
const costCenterData = Object.keys(defaultProfits).map((key, idx) => {
const ccVal = defaultProfits[key];
const costCenterData = allProfitCenters.map((key, idx) => {
const ccVal = key; // defaultProfits[key];
const sale_labor =
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 });
const sale_parts =
@@ -113,11 +124,11 @@ export function JobCostingModalComponent({ bodyshop, job }) {
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const cost = (billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })).add(
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
);
const costs = (
billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
const totalSales = sale_labor.add(sale_parts);
const gpdollars = totalSales.subtract(cost);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / totalSales.getAmount()) *
100
@@ -137,16 +148,19 @@ export function JobCostingModalComponent({ bodyshop, job }) {
.add(sale_parts);
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
summaryData.totalCost = summaryData.totalCost.add(cost);
summaryData.totalCost = summaryData.totalCost.add(costs);
return {
id: idx,
cost_center: ccVal,
sale_labor: sale_labor && sale_labor.toFormat(),
sale_parts: sale_parts && sale_parts.toFormat(),
sales: sale_labor.add(sale_parts).toFormat(),
sales_dinero: sale_labor.add(sale_parts),
cost_parts: cost_parts && cost_parts.toFormat(),
cost_labor: cost_labor && cost_labor.toFormat(),
cost: cost && cost.toFormat(),
costs: cost_parts.add(cost_labor).toFormat(),
costs_dinero: cost_parts.add(cost_labor),
gpdollars: gpdollars.toFormat(),
gppercent: gppercentFormatted,
};
@@ -170,7 +184,23 @@ export function JobCostingModalComponent({ bodyshop, job }) {
return (
<div>
<JobCostingStatistics job={job} summaryData={summaryData} />
<JobCostingPartsTable job={job} data={costCenterData} />
<JobCostingPartsTable
job={job}
data={costCenterData}
summaryData={summaryData}
/>
<div className="imex-flex-row">
<div style={{ flex: 1 }}>
<Typography.Title level={4}>
{t("jobs.labels.sales")}
</Typography.Title>
<JobCostingPie type="sales" costCenterData={costCenterData} />
</div>
<div style={{ flex: 1 }}>
<Typography.Title level={4}>{t("jobs.labels.cost")}</Typography.Title>
<JobCostingPie type="cost" costCenterData={costCenterData} />
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@apollo/react-hooks";
import { useQuery } from "@apollo/client";
import { Modal } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -37,10 +37,11 @@ export function JobCostingModalContainer({
<Modal
visible={visible}
title={t("jobs.labels.jobcosting")}
onOk={() => toggleModalVisible()}
onCancel={() => toggleModalVisible()}
cancelButtonProps={{ style: { display: "none" } }}
width="90%"
destroyOnClose
forceRender
>
{error ? <AlertComponent message={error.message} type="error" /> : null}
{loading ? (

View File

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

View File

@@ -1,9 +1,10 @@
import { Table } from "antd";
import { Input, Table, Typography } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function JobCostingPartsTable({ job, data }) {
export default function JobCostingPartsTable({ job, data, summaryData }) {
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {},
});
@@ -24,37 +25,23 @@ export default function JobCostingPartsTable({ job, data }) {
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
},
{
title: t("jobs.labels.sale_labor"),
dataIndex: "sale_labor",
key: "sale_labor",
sorter: (a, b) => alphaSort(a.sale_labor, b.sale_labor),
title: t("jobs.labels.sales"),
dataIndex: "sales",
key: "sales",
sorter: (a, b) => alphaSort(a.sales, b.sales),
sortOrder:
state.sortedInfo.columnKey === "sale_labor" && state.sortedInfo.order,
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
},
{
title: t("jobs.labels.sale_parts"),
dataIndex: "sale_parts",
key: "sale_parts",
sorter: (a, b) => alphaSort(a.sale_parts, b.sale_parts),
title: t("jobs.labels.costs"),
dataIndex: "costs",
key: "costs",
sorter: (a, b) => a.costs - b.costs,
sortOrder:
state.sortedInfo.columnKey === "sale_parts" && state.sortedInfo.order,
},
{
title: t("jobs.labels.cost_labor"),
dataIndex: "cost_labor",
key: "cost_labor",
sorter: (a, b) => a.cost_labor - b.cost_labor,
sortOrder:
state.sortedInfo.columnKey === "cost_labor" && state.sortedInfo.order,
},
{
title: t("jobs.labels.cost_parts"),
dataIndex: "cost_parts",
key: "cost_parts",
sorter: (a, b) => a.cost_parts - b.cost_parts,
sortOrder:
state.sortedInfo.columnKey === "cost_parts" && state.sortedInfo.order,
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
},
{
title: t("jobs.labels.gpdollars"),
dataIndex: "gpdollars",
@@ -73,16 +60,61 @@ export default function JobCostingPartsTable({ job, data }) {
},
];
const filteredData =
searchText === ""
? data
: data.filter((d) =>
(d.cost_center || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
);
return (
<div>
<Table
size="small"
title={() => {
return (
<div className="imex-table-header">
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</div>
</div>
);
}}
scroll={{ x: "50%", y: "40rem" }}
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}
dataSource={filteredData}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.totalSales.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.totalCost.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.gpdollars.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>
);

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