Merge branch 'dev-patrick' into development
11
README.MD
@@ -1,7 +1,7 @@
|
||||
React App:
|
||||
|
||||
Yarn Dependency Management:
|
||||
To force upgrades for some packages:
|
||||
To force upgrades for some packages:
|
||||
yarn upgrade-interactive --latest
|
||||
|
||||
GraphQL API:
|
||||
@@ -14,12 +14,13 @@ npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
|
||||
Migrating to Staging:
|
||||
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!
|
||||
|
||||
NGROK TEsting:
|
||||
NGROK TEsting:
|
||||
|
||||
./ngrok.exe http https://localhost:5000 -host-header="localhost:5000"
|
||||
|
||||
Finding deadfiles - run from client directory
|
||||
npx deadfile ./src/index.js --exclude build templates
|
||||
|
||||
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
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.1.2",
|
||||
"@testing-library/jest-dom": "^5.11.2",
|
||||
"@testing-library/react": "^10.4.8",
|
||||
"@testing-library/user-event": "^12.1.0",
|
||||
"@apollo/client": "^3.2.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.0.4",
|
||||
"@testing-library/user-event": "^12.1.6",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-logger": "^1.2.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"firebase": "^7.17.1",
|
||||
"firebase": "^7.21.0",
|
||||
"graphql": "^15.3.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-hasura-graphql": "^0.1.12",
|
||||
"react": "^16.13.1",
|
||||
"react-admin": "^3.7.2",
|
||||
"react-admin": "^3.8.5",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-icons": "^3.10.0",
|
||||
"react-scripts": "3.4.1"
|
||||
"react-icons": "^3.11.0",
|
||||
"react-scripts": "3.4.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "set PORT=3001 && react-scripts start",
|
||||
|
||||
806
admin/yarn.lock
35
cert.pem
@@ -1,35 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGJzCCBA+gAwIBAgIUbtAGUWRU8Q+YGMqPt+ETB8uGnA4wDQYJKoZIhvcNAQEL
|
||||
BQAwgaIxCzAJBgNVBAYTAkNBMRkwFwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIw
|
||||
EAYDVQQHDAlWYW5jb3V2ZXIxHDAaBgNVBAoME1NuYXB0IFNvZnR3YXJlIEluYy4x
|
||||
DjAMBgNVBAsMBVNuYXB0MRIwEAYDVQQDDAlsb2NhbGhvc3QxIjAgBgkqhkiG9w0B
|
||||
CQEWE3NuYXB0c29mdEBnbWFpbC5jb20wHhcNMjAwMTE1MDUwMDU4WhcNMjEwMTE0
|
||||
MDUwMDU4WjCBojELMAkGA1UEBhMCQ0ExGTAXBgNVBAgMEEJyaXRpc2ggQ29sdW1i
|
||||
aWExEjAQBgNVBAcMCVZhbmNvdXZlcjEcMBoGA1UECgwTU25hcHQgU29mdHdhcmUg
|
||||
SW5jLjEOMAwGA1UECwwFU25hcHQxEjAQBgNVBAMMCWxvY2FsaG9zdDEiMCAGCSqG
|
||||
SIb3DQEJARYTc25hcHRzb2Z0QGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggIPADCCAgoCggIBAKIo9ImyCPk1+3jWiO8wC9zg3q/C9ZMUeGfoHVCf4N1WM5H6
|
||||
ld/isIQfnEiGIpf7cxpHIazoGkMnBTC4vdOp4JVH8C3ObOZXZiMUg6EDxRJCdR7B
|
||||
ooOnYORV8MWYY0jCpgJ1fRVOs7c3CxzKD9q76r6/+UI2byIb1V51FXl80WuSmnsY
|
||||
+E2DSLViG7lzG/bWL/GQoqhUuDt3UtjFJiBEV89AvHunETGOnZ3TkGfAygum7cid
|
||||
pGHh1O1w4TeuCYGukIOYeY0EgK8LXOl1ILFwVom7/uN/ekp8KfRWPBbj2Oy54SSp
|
||||
/oYSLCOJCGEnOpBFAe+hulQE1CVynMpzl7WID8uS6HxYszCfzPe0FZUTWn0hbULH
|
||||
4z6EabdGskK0mb0ySHZq+fjI119BUoMKoLERC+HZw7nrD2TBVb9LLwwSm7+lneVH
|
||||
wcG3tml+XUOSi7dV5gdMvW72ympnWD5jPWI0PkrH1e7veww/UlixDwzQ8QwduQNc
|
||||
qTRoHUCmGNljUSHavTcN61hiLG6NyFoeErOPoR30LNixA014W/FORXDcDCzvfsHd
|
||||
XhXUFAAODLKs4XTRV/b0sTQL+xi14FlLdQhNYQH8LVTpDZNFmsdlYih2iunsd1gU
|
||||
JGmdwyA5pFyxAp8veJiA/KU2mugfUBoVk8BPz7rzLZSGSvzBV9ZuoIFcrBFzAgMB
|
||||
AAGjUzBRMB0GA1UdDgQWBBRW+mW0lAVmhaCbX3SZbQPQhdTgTjAfBgNVHSMEGDAW
|
||||
gBRW+mW0lAVmhaCbX3SZbQPQhdTgTjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4ICAQA92XAb3LiUpRYWI6w3E6Non95b4pR5sNypkHedCTP2TZyPN16H
|
||||
CPtxJYV6MC3HVbpDcjo4KHrq5QIuZF+w+U3rp06DVgjBMghtoOvOBWTMtJJW8KWS
|
||||
OzJFtdI2+WrJL4n9RcUQ0TFKja82y7HSinCwPHcoy+tIk5pdjDVC8w4N/5/slVJi
|
||||
k418QqwM6CIWtZC5Hf1EbCKwzuR+0x5Q0UY5YeqnLSyR7qjqjp2ZgI/apdNmlQWq
|
||||
M47iPGQtvyK8m0Z7Tw7mScaSd2ZIhQY5E390cGbGnnebOeKa2Ev+kfbG32dMhzQC
|
||||
T9a+7k8IEX7Nak4T2FVKyCzfTFd0n7E6+R1Z0+r20e4M1DGScZIX8iMjs8Gc4Dlc
|
||||
pStTulz+OjsvzRvfYhuKQimGA+z62jehmhd9gDr2IMEpBk7T2OoYsFq+9ulb9z+X
|
||||
PPIOR4vYB4TUDgNbELypvrzHZvQjIyPT3sSq7J5ri151uhWiVoM4/IfnpQ0R0ysB
|
||||
WR3b6Kz8qaQyXO/KAvWLUjnHbW2I8PxfUWmqigc9fb9TjLIAeSl94UR4NvloPIcS
|
||||
FOq2pFMqYHBPi1SEET7WIj0BwDGpMh8OVc6iZ4Rgm43dTHneU2mcyQf5kn6IE0Bd
|
||||
bE0NLRGxW6I/L4kwtQFynkZxxvpeGq9d0gtfe5Qd8AscGL+da40/S95pTQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -7,47 +7,51 @@
|
||||
"@lourenci/react-kanban": "^2.0.0",
|
||||
"@stripe/react-stripe-js": "^1.1.2",
|
||||
"@stripe/stripe-js": "^1.9.0",
|
||||
"@tanem/react-nprogress": "^3.0.40",
|
||||
"@tinymce/tinymce-react": "^3.6.0",
|
||||
"antd": "^4.6.1",
|
||||
"@tanem/react-nprogress": "^3.0.46",
|
||||
"@tinymce/tinymce-react": "^3.7.0",
|
||||
"antd": "^4.6.6",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"apollo-link-context": "^1.0.20",
|
||||
"apollo-link-error": "^1.1.13",
|
||||
"apollo-link-logger": "^1.2.3",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"apollo-link-retry": "^2.2.16",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"axios": "^0.20.0",
|
||||
"codemirror": "^5.58.1",
|
||||
"codemirror-graphql": "^0.12.2",
|
||||
"dinero.js": "^1.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fingerprintjs2": "^2.1.2",
|
||||
"firebase": "^7.19.0",
|
||||
"firebase": "^7.22.1",
|
||||
"graphql": "^15.3.0",
|
||||
"i18next": "^19.7.0",
|
||||
"i18next": "^19.8.2",
|
||||
"i18next-browser-languagedetector": "^6.0.1",
|
||||
"inline-css": "^2.6.3",
|
||||
"logrocket": "^1.0.11",
|
||||
"jsoneditor": "^9.1.1",
|
||||
"jsoneditor-react": "^3.0.1",
|
||||
"logrocket": "^1.0.13",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"phone": "^2.4.15",
|
||||
"phone": "^2.4.16",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.13.1",
|
||||
"query-string": "^6.13.5",
|
||||
"react": "^16.13.1",
|
||||
"react-apollo": "^3.1.5",
|
||||
"react-barcode": "^1.4.0",
|
||||
"react-big-calendar": "^0.26.1",
|
||||
"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",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^1.0.0",
|
||||
"react-i18next": "^11.7.1",
|
||||
"react-grid-layout": "^1.1.1",
|
||||
"react-i18next": "^11.7.3",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-image-file-resizer": "^0.3.6",
|
||||
"react-moment": "^0.9.7",
|
||||
"react-moment": "^1.0.0",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-resizable": "^1.10.1",
|
||||
"react-resizable": "^1.11.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.3",
|
||||
"react-trello": "^2.2.8",
|
||||
@@ -58,7 +62,7 @@
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-state-sync": "^3.1.2",
|
||||
"reselect": "^4.0.0",
|
||||
"styled-components": "^5.1.1",
|
||||
"styled-components": "^5.2.0",
|
||||
"subscriptions-transport-ws": "^0.9.18"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -86,7 +90,7 @@
|
||||
"devDependencies": {
|
||||
"@apollo/react-testing": "^4.0.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.3",
|
||||
"enzyme-adapter-react-16": "^1.15.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.0"
|
||||
}
|
||||
|
||||
BIN
client/public/kavia.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
@@ -1,85 +1,58 @@
|
||||
/* body {
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
line-height: 1.25;
|
||||
} */
|
||||
padding: 10mm 10mm 10mm 10mm !important;
|
||||
}
|
||||
|
||||
table {
|
||||
@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 caption {
|
||||
font-size: 1.5em;
|
||||
table.imex-table caption {
|
||||
/* font-size: 1.5em; */
|
||||
margin: 0.5em 0 0.75em;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
table tr {
|
||||
background-color: #f8f8f8;
|
||||
table.imex-table tr {
|
||||
/* background-color: #f8f8f8; */
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.35em;
|
||||
padding: 0.2rem;
|
||||
font-size: inherit;
|
||||
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 0.625em;
|
||||
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 th {
|
||||
font-size: 0.85em;
|
||||
table.imex-table th {
|
||||
/* font-size: 0.85em; */
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table caption {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
table thead {
|
||||
border: none;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-bottom: 3px solid #ddd;
|
||||
display: block;
|
||||
margin-bottom: 0.625em;
|
||||
}
|
||||
|
||||
table td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table td::before {
|
||||
/*
|
||||
* aria-label has no advantage, it won't be read inside a table
|
||||
content: attr(aria-label);
|
||||
*/
|
||||
content: attr(data-label);
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
table td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* display: table-header-group; */
|
||||
}
|
||||
|
||||
BIN
client/public/vorfahrt.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
@@ -22,7 +22,7 @@ import App from "./App";
|
||||
|
||||
moment.locale("en-US");
|
||||
|
||||
axios.interceptors.request.use(
|
||||
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||
async (config) => {
|
||||
if (!config.headers.Authorization) {
|
||||
const token =
|
||||
@@ -37,6 +37,9 @@ axios.interceptors.request.use(
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
export const cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
@@ -136,8 +139,11 @@ export const client = new ApolloClient({
|
||||
cache,
|
||||
connectToDevTools: process.env.NODE_ENV !== "production",
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
watchQuery: {
|
||||
fetchPolicy: "cache-and-network",
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,5 +68,5 @@
|
||||
|
||||
.ant-table-cell {
|
||||
// background-color: red;
|
||||
padding: 0.2rem !important;
|
||||
//padding: 0.2rem !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
PaymentRequestButtonElement,
|
||||
useStripe
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
@@ -72,7 +72,7 @@ function Test({ bodyshop, setEmailOptions }) {
|
||||
replyTo: bodyshop.email,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList.parts_order_confirmation.key,
|
||||
name: TemplateList().parts_order_confirmation.key,
|
||||
variables: {
|
||||
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
||||
},
|
||||
|
||||
@@ -4,18 +4,15 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import InvoiceExportButton from "../invoice-export-button/invoice-export-button.component";
|
||||
import InvoiceExportAllButton from "../invoice-export-all-button/invoice-export-all-button.component";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function AccountingPayablesTableComponent({
|
||||
loading,
|
||||
invoices,
|
||||
}) {
|
||||
export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedInvoices, setSelectedInvoices] = useState([]);
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
@@ -28,7 +25,7 @@ export default function AccountingPayablesTableComponent({
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("invoices.fields.vendorname"),
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
@@ -46,7 +43,7 @@ export default function AccountingPayablesTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.invoice_number"),
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
@@ -56,9 +53,9 @@ export default function AccountingPayablesTableComponent({
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/invoices`,
|
||||
pathname: `/manage/bills`,
|
||||
search: queryString.stringify({
|
||||
invoiceid: record.id,
|
||||
billid: record.id,
|
||||
vendorid: record.vendor.id,
|
||||
}),
|
||||
}}
|
||||
@@ -79,7 +76,7 @@ export default function AccountingPayablesTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.date"),
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
|
||||
@@ -89,7 +86,7 @@ export default function AccountingPayablesTableComponent({
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.total"),
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
|
||||
@@ -101,7 +98,7 @@ export default function AccountingPayablesTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.is_credit_memo"),
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
@@ -120,8 +117,8 @@ export default function AccountingPayablesTableComponent({
|
||||
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<InvoiceExportButton
|
||||
invoiceId={record.id}
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
/>
|
||||
@@ -136,7 +133,7 @@ export default function AccountingPayablesTableComponent({
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? invoices.filter(
|
||||
? bills.filter(
|
||||
(v) =>
|
||||
(v.vendor.name || "")
|
||||
.toLowerCase()
|
||||
@@ -145,7 +142,7 @@ export default function AccountingPayablesTableComponent({
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
)
|
||||
: invoices;
|
||||
: bills;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -160,11 +157,11 @@ export default function AccountingPayablesTableComponent({
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
<InvoiceExportAllButton
|
||||
invoiceIds={selectedInvoices}
|
||||
disabled={transInProgress || selectedInvoices.length === 0}
|
||||
<PayableExportAll
|
||||
billIds={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedInvoices}
|
||||
completedCallback={setSelectedBills}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -177,14 +174,14 @@ export default function AccountingPayablesTableComponent({
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedInvoices(selectedRows.map((i) => i.id)),
|
||||
setSelectedBills(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedInvoices(selectedRows.map((i) => i.id));
|
||||
setSelectedBills(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedInvoices,
|
||||
selectedRowKeys: selectedBills,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -153,19 +153,20 @@ export default function AccountingPayablesTableComponent({
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<div className="imex-table-header">
|
||||
<PaymentsExportAllButton
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress || selectedPayments.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
/>
|
||||
<Input
|
||||
className="imex-table-header__search"
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
<PaymentsExportAllButton
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -9,11 +9,12 @@ export default function BarcodePopupComponent({ value, children }) {
|
||||
<Popover
|
||||
content={
|
||||
<Barcode
|
||||
value={value}
|
||||
background='transparent'
|
||||
value={value || ""}
|
||||
background="transparent"
|
||||
displayValue={false}
|
||||
/>
|
||||
}>
|
||||
}
|
||||
>
|
||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||
import { Button, Form } from "antd";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import {
|
||||
INSERT_NEW_BILL_LINES,
|
||||
UPDATE_BILL_LINE,
|
||||
} from "../../graphql/bill-lines.queries";
|
||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
|
||||
export default function BillDetailEditcontainer() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [update_bill] = useMutation(UPDATE_BILL);
|
||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
||||
variables: { billid: search.billid },
|
||||
skip: !!!search.billid,
|
||||
});
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
const { billlines, upload, ...bill } = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
update_bill({
|
||||
variables: { billId: search.billid, bill: bill },
|
||||
})
|
||||
);
|
||||
|
||||
billlines.forEach((il) => {
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
updates.push(
|
||||
updateBillLine({
|
||||
variables: {
|
||||
billLineId: il.id,
|
||||
billLine: {
|
||||
...il,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
//It's a new line, have to insert it.
|
||||
updates.push(
|
||||
insertBillLine({
|
||||
variables: {
|
||||
billLines: [
|
||||
{
|
||||
...il,
|
||||
billid: search.billid,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (search.billid) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, search.billid]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (!!!search.billid) return <div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
|
||||
return (
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={
|
||||
data
|
||||
? {
|
||||
...data.bills_by_pk,
|
||||
|
||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
||||
false,
|
||||
state:
|
||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
||||
false,
|
||||
local:
|
||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
||||
false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={exported}
|
||||
loading={updateLoading}
|
||||
type="primary"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
<JobDocumentsGallery
|
||||
jobId={data ? data.bills_by_pk.jobid : null}
|
||||
billId={search.billid}
|
||||
documentsList={data ? data.bills_by_pk.documents : []}
|
||||
billsCallback={refetch}
|
||||
/>
|
||||
</Form>
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
}
|
||||
@@ -4,28 +4,28 @@ import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_INVOICE } from "../../graphql/invoices.queries";
|
||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
import InvoiceFormContainer from "../invoice-form/invoice-form.container";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
invoiceEnterModal: selectInvoiceEnterModal,
|
||||
billEnterModal: selectBillEnterModal,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
});
|
||||
|
||||
function InvoiceEnterModalContainer({
|
||||
invoiceEnterModal,
|
||||
function BillEnterModalContainer({
|
||||
billEnterModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
@@ -33,38 +33,41 @@ function InvoiceEnterModalContainer({
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertInvoice] = useMutation(INSERT_NEW_INVOICE);
|
||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = (values) => {
|
||||
setLoading(true);
|
||||
const { upload, ...remainingValues } = values;
|
||||
insertInvoice({
|
||||
const { upload, location, ...remainingValues } = values;
|
||||
insertBill({
|
||||
variables: {
|
||||
invoice: [
|
||||
bill: [
|
||||
Object.assign({}, remainingValues, {
|
||||
invoicelines: {
|
||||
data: remainingValues.invoicelines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||
};
|
||||
}),
|
||||
billlines: {
|
||||
data:
|
||||
remainingValues.billlines &&
|
||||
remainingValues.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const invoiceId = r.data.insert_invoices.returning[0].id;
|
||||
const billId = r.data.insert_bills.returning[0].id;
|
||||
|
||||
updateJobLines({
|
||||
variables: {
|
||||
ids: remainingValues.invoicelines
|
||||
ids: remainingValues.billlines
|
||||
.filter((il) => il.joblineid !== "noline")
|
||||
.map((li) => li.joblineid),
|
||||
status: bodyshop.md_order_statuses.default_received || "Received*",
|
||||
location: location,
|
||||
},
|
||||
}).then((joblineresult) => {
|
||||
/////////////////////////
|
||||
@@ -77,7 +80,7 @@ function InvoiceEnterModalContainer({
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
invoiceId: invoiceId,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null,
|
||||
}
|
||||
@@ -87,14 +90,13 @@ function InvoiceEnterModalContainer({
|
||||
///////////////////////////
|
||||
setLoading(false);
|
||||
notification["success"]({
|
||||
message: t("invoices.successes.created"),
|
||||
message: t("bills.successes.created"),
|
||||
});
|
||||
if (invoiceEnterModal.actions.refetch)
|
||||
invoiceEnterModal.actions.refetch();
|
||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ invoicelines: [] });
|
||||
form.setFieldsValue({ billlines: [] });
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
@@ -105,7 +107,7 @@ function InvoiceEnterModalContainer({
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("invoices.errors.creating", {
|
||||
message: t("bills.errors.creating", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
@@ -121,14 +123,14 @@ function InvoiceEnterModalContainer({
|
||||
}, [enterAgain, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (invoiceEnterModal.visible) form.resetFields();
|
||||
}, [invoiceEnterModal.visible, form]);
|
||||
if (billEnterModal.visible) form.resetFields();
|
||||
}, [billEnterModal.visible, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("invoices.labels.new")}
|
||||
title={t("bills.labels.new")}
|
||||
width={"90%"}
|
||||
visible={invoiceEnterModal.visible}
|
||||
visible={billEnterModal.visible}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={handleCancel}
|
||||
@@ -139,7 +141,7 @@ function InvoiceEnterModalContainer({
|
||||
<Button loading={loading} onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{invoiceEnterModal.context && invoiceEnterModal.context.id ? null : (
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
@@ -157,22 +159,31 @@ function InvoiceEnterModalContainer({
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinishFailed={() => {
|
||||
setEnterAgain(false);
|
||||
}}
|
||||
initialValues={{
|
||||
...invoiceEnterModal.context.invoice,
|
||||
...billEnterModal.context.bill,
|
||||
jobid:
|
||||
(invoiceEnterModal.context.job &&
|
||||
invoiceEnterModal.context.job.id) ||
|
||||
(billEnterModal.context.job && billEnterModal.context.job.id) ||
|
||||
null,
|
||||
federal_tax_rate: bodyshop.invoice_tax_rates.federal_tax_rate || 0,
|
||||
state_tax_rate: bodyshop.invoice_tax_rates.state_tax_rate || 0,
|
||||
local_tax_rate: bodyshop.invoice_tax_rates.local_tax_rate || 0,
|
||||
federal_tax_rate:
|
||||
(bodyshop.bill_tax_rates &&
|
||||
bodyshop.bill_tax_rates.federal_tax_rate) ||
|
||||
0,
|
||||
state_tax_rate:
|
||||
(bodyshop.bill_tax_rates &&
|
||||
bodyshop.bill_tax_rates.state_tax_rate) ||
|
||||
0,
|
||||
local_tax_rate:
|
||||
(bodyshop.bill_tax_rates &&
|
||||
bodyshop.bill_tax_rates.local_tax_rate) ||
|
||||
0,
|
||||
}}
|
||||
>
|
||||
<InvoiceFormContainer form={form} />
|
||||
<BillFormContainer form={form} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
@@ -181,4 +192,4 @@ function InvoiceEnterModalContainer({
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InvoiceEnterModalContainer);
|
||||
)(BillEnterModalContainer);
|
||||
@@ -1,26 +1,46 @@
|
||||
import { Button, Form, Input, Statistic, Switch, Upload } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Typography,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
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 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 InvoiceFormLines from "./invoice-form.lines.component";
|
||||
import "./invoice-form.styles.scss";
|
||||
import { CalculateInvoiceTotal } from "./invoice-form.totals.utility";
|
||||
import BillFormLines from "./bill-form.lines.component";
|
||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default function InvoiceFormComponent({
|
||||
export function BillFormComponent({
|
||||
bodyshop,
|
||||
disabled,
|
||||
form,
|
||||
roAutoCompleteOptions,
|
||||
vendorAutoCompleteOptions,
|
||||
lineData,
|
||||
responsibilityCenters,
|
||||
loadLines,
|
||||
invoiceEdit,
|
||||
billEdit,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const client = useApolloClient();
|
||||
const [discount, setDiscount] = useState(0);
|
||||
|
||||
const handleVendorSelect = (props, opt) => {
|
||||
@@ -44,11 +64,11 @@ export default function InvoiceFormComponent({
|
||||
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
|
||||
|
||||
return (
|
||||
<div className="invoice-form-wrapper">
|
||||
<div className="invoice-form-invoice-details">
|
||||
<div>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("invoices.fields.ro_number")}
|
||||
label={t("bills.fields.ro_number")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -57,8 +77,7 @@ export default function InvoiceFormComponent({
|
||||
]}
|
||||
>
|
||||
<JobSearchSelect
|
||||
options={roAutoCompleteOptions}
|
||||
disabled={invoiceEdit}
|
||||
disabled={billEdit || disabled}
|
||||
onBlur={() => {
|
||||
if (form.getFieldValue("jobid") !== null) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
@@ -67,9 +86,9 @@ export default function InvoiceFormComponent({
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.vendor")}
|
||||
label={t("bills.fields.vendor")}
|
||||
name="vendorid"
|
||||
style={{ display: invoiceEdit ? "none" : null }}
|
||||
style={{ display: billEdit ? "none" : null }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -78,26 +97,53 @@ export default function InvoiceFormComponent({
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
disabled={disabled}
|
||||
options={vendorAutoCompleteOptions}
|
||||
onSelect={handleVendorSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="invoice-form-invoice-details">
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.invoice_number")}
|
||||
label={t("bills.fields.invoice_number")}
|
||||
name="invoice_number"
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const vendorid = getFieldValue("vendorid");
|
||||
if (vendorid) {
|
||||
const response = await client.query({
|
||||
query: CHECK_BILL_INVOICE_NUMBER,
|
||||
variables: {
|
||||
invoice_number: value,
|
||||
vendorid: vendorid,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("bills.validation.unique_invoice_number")
|
||||
);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.date")}
|
||||
label={t("bills.fields.date")}
|
||||
name="date"
|
||||
rules={[
|
||||
{
|
||||
@@ -106,17 +152,17 @@ export default function InvoiceFormComponent({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
<FormDatePicker disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.is_credit_memo")}
|
||||
label={t("bills.fields.is_credit_memo")}
|
||||
name="is_credit_memo"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.total")}
|
||||
label={t("bills.fields.total")}
|
||||
name="total"
|
||||
rules={[
|
||||
{
|
||||
@@ -125,38 +171,52 @@ export default function InvoiceFormComponent({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.federal_tax_rate")}
|
||||
label={t("bills.fields.federal_tax_rate")}
|
||||
name="federal_tax_rate"
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.state_tax_rate")}
|
||||
label={t("bills.fields.state_tax_rate")}
|
||||
name="state_tax_rate"
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoices.fields.local_tax_rate")}
|
||||
label={t("bills.fields.local_tax_rate")}
|
||||
name="local_tax_rate"
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<InvoiceFormLines
|
||||
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} 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>
|
||||
<Typography.Title level={4}>
|
||||
{t("bills.labels.bill_lines")}
|
||||
</Typography.Title>
|
||||
<BillFormLines
|
||||
lineData={lineData}
|
||||
discount={discount}
|
||||
form={form}
|
||||
responsibilityCenters={responsibilityCenters}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="upload"
|
||||
label="Upload"
|
||||
style={{ display: invoiceEdit ? "none" : null }}
|
||||
style={{ display: billEdit ? "none" : null }}
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={(e) => {
|
||||
if (Array.isArray(e)) {
|
||||
@@ -173,7 +233,7 @@ export default function InvoiceFormComponent({
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const values = form.getFieldsValue([
|
||||
"invoicelines",
|
||||
"billlines",
|
||||
"total",
|
||||
"federal_tax_rate",
|
||||
"state_tax_rate",
|
||||
@@ -182,46 +242,46 @@ export default function InvoiceFormComponent({
|
||||
let totals;
|
||||
if (
|
||||
!!values.total &&
|
||||
!!values.invoicelines &&
|
||||
values.invoicelines.length > 0
|
||||
!!values.billlines &&
|
||||
values.billlines.length > 0
|
||||
)
|
||||
totals = CalculateInvoiceTotal(values);
|
||||
totals = CalculateBillTotal(values);
|
||||
if (!!totals)
|
||||
return (
|
||||
<div>
|
||||
<div className="invoice-form-totals">
|
||||
<Space>
|
||||
<Statistic
|
||||
title={t("invoices.labels.subtotal")}
|
||||
title={t("bills.labels.subtotal")}
|
||||
value={totals.subtotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.federal_tax")}
|
||||
title={t("bills.labels.federal_tax")}
|
||||
value={totals.federalTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.state_tax")}
|
||||
title={t("bills.labels.state_tax")}
|
||||
value={totals.stateTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.local_tax")}
|
||||
title={t("bills.labels.local_tax")}
|
||||
value={totals.localTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.entered_total")}
|
||||
title={t("bills.labels.entered_total")}
|
||||
value={totals.enteredTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.invoice_total")}
|
||||
title={t("bills.labels.bill_total")}
|
||||
value={totals.invoiceTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.discrepancy")}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color:
|
||||
totals.discrepancy.getAmount() === 0 ? "green" : "red",
|
||||
@@ -229,11 +289,11 @@ export default function InvoiceFormComponent({
|
||||
value={totals.discrepancy.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
{form.getFieldValue("is_credit_memo") ? (
|
||||
<AlertComponent
|
||||
type="warning"
|
||||
message={t("invoices.labels.enteringcreditmemo")}
|
||||
message={t("bills.labels.enteringcreditmemo")}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -244,3 +304,5 @@ export default function InvoiceFormComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);
|
||||
35
client/src/components/bill-form/bill-form.container.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useLazyQuery, useQuery } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import BillFormComponent from "./bill-form.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled }) {
|
||||
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
|
||||
|
||||
const [loadLines, { data: lineData }] = useLazyQuery(
|
||||
GET_JOB_LINES_TO_ENTER_BILL
|
||||
);
|
||||
|
||||
return (
|
||||
<BillFormComponent
|
||||
disabled={disabled}
|
||||
form={form}
|
||||
billEdit={billEdit}
|
||||
vendorAutoCompleteOptions={
|
||||
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
||||
}
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(BillFormContainer);
|
||||
248
client/src/components/bill-form/bill-form.lines.component.jsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Switch,
|
||||
} 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 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({
|
||||
disabled,
|
||||
lineData,
|
||||
discount,
|
||||
form,
|
||||
responsibilityCenters,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { setFieldsValue, getFieldsValue } = form;
|
||||
|
||||
return (
|
||||
<Form.List name="billlines">
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div className="invoice-form-lines-wrapper">
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item required={false} key={field.key}>
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<LayoutFormRow style={{ flex: 1 }} grow>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.jobline")}
|
||||
key={`${index}joblinename`}
|
||||
name={[field.name, "joblineid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<BillLineSearchSelect
|
||||
disabled={disabled}
|
||||
options={lineData}
|
||||
onSelect={(value, opt) => {
|
||||
setFieldsValue({
|
||||
billlines: getFieldsValue([
|
||||
"billlines",
|
||||
]).billlines.map((item, idx) => {
|
||||
if (idx === index) {
|
||||
return {
|
||||
...item,
|
||||
line_desc: opt.line_desc,
|
||||
quantity: opt.part_qty || 1,
|
||||
actual_price: opt.cost,
|
||||
cost_center: opt.part_type
|
||||
? responsibilityCenters.defaults.costs[
|
||||
opt.part_type
|
||||
] || null
|
||||
: null,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("billlines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.actual")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
onBlur={(e) => {
|
||||
setFieldsValue({
|
||||
billlines: getFieldsValue(
|
||||
"billlines"
|
||||
).billlines.map((item, idx) => {
|
||||
if (idx === index) {
|
||||
return {
|
||||
...item,
|
||||
actual_cost: !!item.actual_cost
|
||||
? item.actual_cost
|
||||
: parseFloat(e.target.value) *
|
||||
(1 - discount),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.actual_cost")}
|
||||
key={`${index}actual_cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const line = getFieldsValue(["billlines"]).billlines[
|
||||
index
|
||||
];
|
||||
if (!!!line) return null;
|
||||
const lineDiscount = (
|
||||
1 -
|
||||
Math.round(
|
||||
(line.actual_cost / line.actual_price) * 100
|
||||
) /
|
||||
100
|
||||
).toPrecision(2);
|
||||
|
||||
if (lineDiscount - discount === 0) return <div />;
|
||||
return <WarningOutlined style={{ color: "red" }} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.cost_center")}
|
||||
key={`${index}cost_center`}
|
||||
name={[field.name, "cost_center"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select style={{ width: "150px" }} disabled={disabled}>
|
||||
{responsibilityCenters.costs.map((item) => (
|
||||
<Select.Option key={item.name}>
|
||||
{item.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.federal_tax_applicable")}
|
||||
key={`${index}fedtax`}
|
||||
initialValue={true}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "federal"]}
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.state_tax_applicable")}
|
||||
key={`${index}statetax`}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "state"]}
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.local_tax_applicable")}
|
||||
key={`${index}localtax`}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "local"]}
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
<DeleteFilled
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("billlines.actions.newline")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import Dinero from "dinero.js";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export const CalculateInvoiceTotal = (invoice) => {
|
||||
export const CalculateBillTotal = (invoice) => {
|
||||
logImEXEvent("invoice_calculate_total");
|
||||
|
||||
const {
|
||||
total,
|
||||
|
||||
invoicelines,
|
||||
billlines,
|
||||
federal_tax_rate,
|
||||
local_tax_rate,
|
||||
state_tax_rate,
|
||||
@@ -17,8 +16,10 @@ export const CalculateInvoiceTotal = (invoice) => {
|
||||
let federalTax = Dinero({ amount: 0 });
|
||||
let stateTax = Dinero({ amount: 0 });
|
||||
let localTax = Dinero({ amount: 0 });
|
||||
if (!!!invoicelines) return null;
|
||||
invoicelines.forEach((i) => {
|
||||
|
||||
if (!!!billlines) return null;
|
||||
|
||||
billlines.forEach((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({
|
||||
amount: Math.round((i.actual_cost || 0) * 100) || 0,
|
||||
@@ -5,8 +5,8 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
|
||||
//To be used as a form element only.
|
||||
const { Option } = Select;
|
||||
const InvoiceLineSearchSelect = (
|
||||
{ value, onChange, options, onBlur, onSelect },
|
||||
const BillLineSearchSelect = (
|
||||
{ value, onChange, options, onBlur, onSelect, disabled },
|
||||
ref
|
||||
) => {
|
||||
const [option, setOption] = useState(value);
|
||||
@@ -20,12 +20,13 @@ const InvoiceLineSearchSelect = (
|
||||
|
||||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
showSearch
|
||||
autoFocus
|
||||
value={option}
|
||||
style={{
|
||||
width: 300,
|
||||
width: "100%",
|
||||
}}
|
||||
onChange={setOption}
|
||||
optionFilterProp="line_desc"
|
||||
@@ -33,7 +34,7 @@ const InvoiceLineSearchSelect = (
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("invoicelines.labels.other")}
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
@@ -48,12 +49,18 @@ const InvoiceLineSearchSelect = (
|
||||
<Row justify="center" align="middle">
|
||||
<Col span={12}>{item.line_desc}</Col>
|
||||
<Col span={8}>
|
||||
<Tag color="blue">{item.oem_partno}</Tag>
|
||||
{item.oem_partno ? (
|
||||
<Tag color="blue">{item.oem_partno}</Tag>
|
||||
) : null}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Tag color="green">
|
||||
<CurrencyFormatter>{item.act_price || 0}</CurrencyFormatter>
|
||||
</Tag>
|
||||
{item.act_price ? (
|
||||
<Tag color="green">
|
||||
<CurrencyFormatter>
|
||||
{item.act_price || 0}
|
||||
</CurrencyFormatter>
|
||||
</Tag>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
</Option>
|
||||
@@ -62,4 +69,4 @@ const InvoiceLineSearchSelect = (
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(InvoiceLineSearchSelect);
|
||||
export default forwardRef(BillLineSearchSelect);
|
||||
@@ -1,51 +1,51 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Checkbox, Descriptions, Table, Input, Typography } from "antd";
|
||||
import { Button, Checkbox, Descriptions, Input, Table, Typography } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import queryString from "query-string";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//jobRO: selectJobReadOnly,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setInvoiceEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setReconciliationContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "reconciliation" })),
|
||||
});
|
||||
|
||||
export function InvoicesListTableComponent({
|
||||
export function BillsListTableComponent({
|
||||
job,
|
||||
loading,
|
||||
invoicesQuery,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsOrderContext,
|
||||
setInvoiceEnterContext,
|
||||
setBillEnterContext,
|
||||
setReconciliationContext,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [
|
||||
selectedInvoiceLinesByInvoice,
|
||||
setSelectedInvoiceLinesByInvoice,
|
||||
] = useState({});
|
||||
const [selectedBillLinesByBill, setSelectedBillLinesByBill] = useState({});
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedInvoice = search.invoiceid;
|
||||
const selectedBill = search.billid;
|
||||
|
||||
const invoices = invoicesQuery.data ? invoicesQuery.data.invoices : [];
|
||||
const { refetch } = invoicesQuery;
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const { refetch } = billsQuery;
|
||||
const columns = [
|
||||
{
|
||||
title: t("invoices.fields.vendorname"),
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
@@ -54,7 +54,7 @@ export function InvoicesListTableComponent({
|
||||
render: (text, record) => <span>{record.vendor.name}</span>,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.invoice_number"),
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
@@ -63,7 +63,7 @@ export function InvoicesListTableComponent({
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.date"),
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => a.date - b.date,
|
||||
@@ -72,7 +72,7 @@ export function InvoicesListTableComponent({
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.total"),
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
@@ -83,7 +83,7 @@ export function InvoicesListTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.is_credit_memo"),
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
@@ -92,17 +92,30 @@ export function InvoicesListTableComponent({
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.is_credit_memo} />,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.exported"),
|
||||
dataIndex: "exported",
|
||||
key: "exported",
|
||||
sorter: (a, b) => a.exported - b.exported,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.exported} />,
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Link
|
||||
to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`}
|
||||
>
|
||||
<Button>{t("invoices.actions.edit")}</Button>
|
||||
</Link>
|
||||
{record.exported ? (
|
||||
<Button disabled>{t("bills.actions.edit")}</Button>
|
||||
) : (
|
||||
<Link
|
||||
to={`/manage/bills?billid=${record.id}&vendorid=${record.vendorid}`}
|
||||
>
|
||||
<Button>{t("bills.actions.edit")}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -115,7 +128,7 @@ export function InvoicesListTableComponent({
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("invoicelines.fields.line_desc"),
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
@@ -123,7 +136,7 @@ export function InvoicesListTableComponent({
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.retail"),
|
||||
title: t("billlines.fields.retail"),
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
||||
@@ -135,7 +148,7 @@ export function InvoicesListTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.actual_cost"),
|
||||
title: t("billlines.fields.actual_cost"),
|
||||
dataIndex: "actual_cost",
|
||||
key: "actual_cost",
|
||||
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
||||
@@ -147,7 +160,7 @@ export function InvoicesListTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.quantity"),
|
||||
title: t("billlines.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
@@ -155,7 +168,7 @@ export function InvoicesListTableComponent({
|
||||
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.cost_center"),
|
||||
title: t("billlines.fields.cost_center"),
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
@@ -164,7 +177,7 @@ export function InvoicesListTableComponent({
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.federal_tax_applicable"),
|
||||
title: t("billlines.fields.federal_tax_applicable"),
|
||||
dataIndex: "applicable_taxes.federal",
|
||||
key: "applicable_taxes.federal",
|
||||
render: (text, record) => (
|
||||
@@ -178,7 +191,7 @@ export function InvoicesListTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.state_tax_applicable"),
|
||||
title: t("billlines.fields.state_tax_applicable"),
|
||||
dataIndex: "applicable_taxes.state",
|
||||
key: "applicable_taxes.state",
|
||||
render: (text, record) => (
|
||||
@@ -192,7 +205,7 @@ export function InvoicesListTableComponent({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.local_tax_applicable"),
|
||||
title: t("billlines.fields.local_tax_applicable"),
|
||||
dataIndex: "applicable_taxes.local",
|
||||
key: "applicable_taxes.local",
|
||||
render: (text, record) => (
|
||||
@@ -207,42 +220,48 @@ export function InvoicesListTableComponent({
|
||||
},
|
||||
];
|
||||
|
||||
const handleOnInvoiceRowclick = (selectedRows) => {
|
||||
setSelectedInvoiceLinesByInvoice({
|
||||
...selectedInvoiceLinesByInvoice,
|
||||
const handleOnBillrowclick = (selectedRows) => {
|
||||
setSelectedBillLinesByBill({
|
||||
...selectedBillLinesByBill,
|
||||
[record.id]: selectedRows.map((r) => r.id),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={3}>{`${t("invoices.fields.invoice_number")} ${
|
||||
<Typography.Title level={3}>{`${t("bills.fields.invoice_number")} ${
|
||||
record.invoice_number
|
||||
}`}</Typography.Title>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label={t("invoices.fields.federal_tax_rate")}>
|
||||
<Descriptions.Item label={t("bills.fields.federal_tax_rate")}>
|
||||
{`${record.federal_tax_rate}%` || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("invoices.fields.state_tax_rate")}>
|
||||
<Descriptions.Item label={t("bills.fields.state_tax_rate")}>
|
||||
{`${record.state_tax_rate}%` || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("invoices.fields.local_tax_rate")}>
|
||||
<Descriptions.Item label={t("bills.fields.local_tax_rate")}>
|
||||
{`${record.local_tax_rate}%` || ""}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Button
|
||||
disabled={record.is_credit_memo}
|
||||
disabled={
|
||||
!selectedBillLinesByBill[record.id] ||
|
||||
(selectedBillLinesByBill[record.id] &&
|
||||
selectedBillLinesByBill[record.id].length === 0) ||
|
||||
record.is_credit_memo ||
|
||||
record.exported
|
||||
}
|
||||
onClick={() =>
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: job.id,
|
||||
vendorId: record.vendorid,
|
||||
returnFromInvoice: record.id,
|
||||
returnFromBill: record.id,
|
||||
invoiceNumber: record.invoice_number,
|
||||
linesToOrder: record.invoicelines
|
||||
linesToOrder: record.billlines
|
||||
.filter((il) =>
|
||||
selectedInvoiceLinesByInvoice[record.id].includes(il.id)
|
||||
selectedBillLinesByBill[record.id].includes(il.id)
|
||||
)
|
||||
.map((i) => {
|
||||
return {
|
||||
@@ -258,7 +277,7 @@ export function InvoicesListTableComponent({
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("invoices.actions.return")}
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
<Table
|
||||
size="small"
|
||||
@@ -266,15 +285,15 @@ export function InvoicesListTableComponent({
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.invoicelines}
|
||||
dataSource={record.billlines}
|
||||
rowSelection={{
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
handleOnInvoiceRowclick(selectedRows);
|
||||
handleOnBillrowclick(selectedRows);
|
||||
},
|
||||
onSelectAll: (selected, selectedRows, changeRows) => {
|
||||
handleOnInvoiceRowclick(selectedRows);
|
||||
handleOnBillrowclick(selectedRows);
|
||||
},
|
||||
selectedRowKeys: selectedInvoiceLinesByInvoice[record.id],
|
||||
selectedRowKeys: selectedBillLinesByBill[record.id],
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
@@ -284,11 +303,9 @@ export function InvoicesListTableComponent({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>
|
||||
{t("invoices.labels.invoices")}
|
||||
</Typography.Title>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Table
|
||||
loading={loading}
|
||||
loading={billsQuery.loading}
|
||||
size="small"
|
||||
title={() => (
|
||||
<div className="imex-table-header">
|
||||
@@ -299,25 +316,23 @@ export function InvoicesListTableComponent({
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInvoiceEnterContext({
|
||||
actions: { refetch: invoicesQuery.refetch },
|
||||
setBillEnterContext({
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
context: {
|
||||
job,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.postInvoices")}
|
||||
{t("jobs.actions.postbills")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setReconciliationContext({
|
||||
actions: { refetch: invoicesQuery.refetch },
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
context: {
|
||||
job,
|
||||
invoices:
|
||||
(invoicesQuery.data && invoicesQuery.data.invoices) ||
|
||||
[],
|
||||
bills: (billsQuery.data && billsQuery.data.bills) || [],
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -342,10 +357,10 @@ export function InvoicesListTableComponent({
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={invoices}
|
||||
dataSource={bills}
|
||||
onChange={handleTableChange}
|
||||
expandable={{
|
||||
expandedRowKeys: [selectedInvoice],
|
||||
expandedRowKeys: [selectedBill],
|
||||
onExpand: (expanded, record) => {
|
||||
handleOnRowClick(expanded ? record : null);
|
||||
},
|
||||
@@ -354,7 +369,7 @@ export function InvoicesListTableComponent({
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [selectedInvoice],
|
||||
selectedRowKeys: [selectedBill],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
@@ -372,4 +387,7 @@ export function InvoicesListTableComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(InvoicesListTableComponent);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillsListTableComponent);
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
export default function InvoicesVendorsList() {
|
||||
export default function BillsVendorsList() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useHistory();
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function InvoicesVendorsList() {
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
delete search.invoiceid;
|
||||
delete search.billid;
|
||||
if (record.id) {
|
||||
search.vendorid = record.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
@@ -6,27 +6,44 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatAffixComponent from "./chat-affix.component";
|
||||
import { Affix } from "antd";
|
||||
import "./chat-affix.styles.scss";
|
||||
export default function ChatAffixContainer() {
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION
|
||||
CONVERSATION_LIST_SUBSCRIPTION,
|
||||
{
|
||||
skip: !bodyshop || (bodyshop && !bodyshop.messagingservicesid),
|
||||
}
|
||||
);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<Affix className='chat-affix'>
|
||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
<div>
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
}
|
||||
|
||||
.chat-affix-open {
|
||||
-webkit-box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
|
||||
-moz-box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
|
||||
box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Badge, List } from "antd";
|
||||
import { Badge, List, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
@@ -22,43 +21,46 @@ export function ChatConversationListComponent({
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List
|
||||
bordered
|
||||
size="small"
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<PhoneFormatter>{item.phone_num}</PhoneFormatter>}
|
||||
description={
|
||||
item.job_conversations.length > 0 ? (
|
||||
<div>
|
||||
{item.job_conversations.map(
|
||||
(j) =>
|
||||
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
t("messaging.labels.nojobs")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<div className="chat-list-container">
|
||||
<List
|
||||
bordered
|
||||
size="small"
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
{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 || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} `}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
)}
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
.chat-list-selected-conversation {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
.chat-list-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #ff7a00;
|
||||
}
|
||||
.chat-name {
|
||||
flex: 1;
|
||||
display: inline;
|
||||
}
|
||||
.ro-number-tag {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
||||
<Tag
|
||||
key={item.job.id}
|
||||
closable
|
||||
color='blue'
|
||||
color="blue"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClose={() => handleRemoveTag(item.job.id)}>
|
||||
onClose={() => handleRemoveTag(item.job.id)}
|
||||
>
|
||||
<Link to={`/manage/jobs/${item.job.id}`}>
|
||||
{item.job.ro_number || "?"}
|
||||
{`${item.job.ro_number || "?"} | ${item.job.ownr_fn || ""} ${
|
||||
item.job.ownr_ln || ""
|
||||
} ${item.job.ownr_co_nm || ""}`}
|
||||
</Link>
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import { Space } from "antd";
|
||||
import React from "react";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<div>
|
||||
<Space>
|
||||
<strong>{conversation && conversation.phone_num}</strong>
|
||||
<span>
|
||||
{conversation.job_conversations.map(
|
||||
(j) =>
|
||||
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} | `
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
<div className="imex-flex-row imex-flex-row__margin">
|
||||
<div className="imex-flex-row">
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
<ChatPresetsComponent />
|
||||
</div>
|
||||
<div className="imex-flex-row">
|
||||
<PhoneNumberFormatter>
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// bottom: 0rem;
|
||||
color: whitesmoke;
|
||||
border: #000000;
|
||||
margin-left: 0.2rem;
|
||||
margin-right: 0rem;
|
||||
// z-index: 5;
|
||||
position: absolute;
|
||||
margin: 0 0.1rem;
|
||||
bottom: 0.1rem;
|
||||
right: 0.3rem;
|
||||
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.chat {
|
||||
@@ -84,6 +87,7 @@
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
position: relative;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.mine .message.last:before {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PlusCircleFilled } from "@ant-design/icons";
|
||||
import { Button, Form, Popover } from "antd";
|
||||
import React from "react";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
});
|
||||
|
||||
export function ChatNewConversation({ openChatByPhone }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const handleFinish = (values) => {
|
||||
console.log("values :>> ", values);
|
||||
openChatByPhone({ phone_num: values.phoneNumber });
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<Form.Item label={t("messaging.labels.phonenumber")} name="phoneNumber">
|
||||
<PhoneFormItem />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("messaging.actions.new")}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover trigger="click" content={popContent}>
|
||||
<PlusCircleFilled style={{ margin: "1rem" }} />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatNewConversation);
|
||||
@@ -9,7 +9,10 @@ export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
||||
return (
|
||||
<MessageFilled
|
||||
style={{ margin: 4 }}
|
||||
onClick={() => openChatByPhone({ phone_num: phone, jobid: jobid })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openChatByPhone({ phone_num: phone, jobid: jobid });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import "./chat-popup.styles.scss";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
@@ -25,15 +26,18 @@ export function ChatPopupComponent({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="chat-popup">
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<ChatNewConversation />
|
||||
</div>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row className="chat-popup-content">
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
<ChatConversationListComponent conversationList={conversationList} />
|
||||
</Col>
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
}
|
||||
|
||||
.chat-popup-content {
|
||||
//height: 50vh;
|
||||
min-height: 0px; /* IMPORTANT: you need this for non-chrome browsers */
|
||||
flex: 1;
|
||||
//height: 100%;
|
||||
.ant-col {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||
@@ -16,9 +15,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
});
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
||||
@@ -30,15 +27,9 @@ export function ChatPresetsComponent({ bodyshop, setMessage }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("messaging.labels.presets")} <DownOutlined />
|
||||
</a>
|
||||
<PlusCircleOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
selectMessage,
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -53,6 +54,8 @@ function ChatSendMessageComponent({
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row">
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
@@ -68,6 +71,7 @@ function ChatSendMessageComponent({
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Select, Empty } from "antd";
|
||||
import React from "react";
|
||||
import { AutoComplete } from "antd";
|
||||
import { LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChatTagRoComponent({
|
||||
searchQueryState,
|
||||
roOptions,
|
||||
loading,
|
||||
executeSearch,
|
||||
handleSearch,
|
||||
handleInsertTag,
|
||||
setVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setSearchQuery = searchQueryState[1];
|
||||
const handleSearchQuery = (value) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
executeSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<AutoComplete
|
||||
style={{ width: 100 }}
|
||||
onSearch={handleSearchQuery}
|
||||
onSelect={handleInsertTag}
|
||||
<div>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
style={{
|
||||
width: 300,
|
||||
}}
|
||||
placeholder={t("general.labels.search")}
|
||||
onKeyDown={handleKeyDown}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<AutoComplete.Option key={item.id || idx}>
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||
item.ownr_ln || ""
|
||||
} ${item.ownr_co_nm || ""}`}
|
||||
</AutoComplete.Option>
|
||||
</Select.Option>
|
||||
))}
|
||||
</AutoComplete>
|
||||
</Select>
|
||||
{loading ? <LoadingOutlined /> : null}
|
||||
|
||||
{loading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import React, { useState } from "react";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
|
||||
import { Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
|
||||
export default function ChatTagRoContainer({ conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const searchQueryState = useState("");
|
||||
const searchText = searchQueryState[0];
|
||||
|
||||
const [loadRo, { called, loading, data, refetch }] = useLazyQuery(
|
||||
SEARCH_FOR_JOBS,
|
||||
{
|
||||
variables: { search: `%${searchText}%` },
|
||||
}
|
||||
);
|
||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||
|
||||
const executeSearch = () => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: searchText });
|
||||
if (called) refetch();
|
||||
else {
|
||||
loadRo();
|
||||
}
|
||||
const executeSearch = (v) => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
||||
loadRo(v);
|
||||
};
|
||||
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
|
||||
@@ -39,10 +36,13 @@ export default function ChatTagRoContainer({ conversation }) {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const existingJobTags = conversation.job_conversations.map((i) => i.jobid);
|
||||
const existingJobTags =
|
||||
conversation &&
|
||||
conversation.job_conversations &&
|
||||
conversation.job_conversations.map((i) => i.jobid);
|
||||
|
||||
const roOptions = data
|
||||
? data.jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -50,9 +50,8 @@ export default function ChatTagRoContainer({ conversation }) {
|
||||
{visible ? (
|
||||
<ChatTagRo
|
||||
loading={loading}
|
||||
searchQueryState={searchQueryState}
|
||||
roOptions={roOptions}
|
||||
executeSearch={executeSearch}
|
||||
handleSearch={handleSearch}
|
||||
handleInsertTag={handleInsertTag}
|
||||
setVisible={setVisible}
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,17 @@ import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ContractCarsComponent from "./contract-cars.component";
|
||||
|
||||
|
||||
|
||||
export default function ContractCarsContainer({ selectedCarState, bodyshop }) {
|
||||
export default function ContractCarsContainer({ selectedCarState, form }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC);
|
||||
|
||||
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||
|
||||
const handleSelect = record => {
|
||||
const handleSelect = (record) => {
|
||||
setSelectedCar(record.id);
|
||||
form.setFieldsValue({
|
||||
kmstart: record.mileage,
|
||||
dailyrate: record.dailycost,
|
||||
});
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
@@ -116,9 +116,9 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract }) {
|
||||
shopid: bodyshop.id,
|
||||
ownerid: contract.job.ownerid,
|
||||
vehicleid: contract.job.vehicleid,
|
||||
federal_tax_rate: bodyshop.invoice_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.invoice_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.invoice_tax_rates.local_tax_rate / 100,
|
||||
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
|
||||
ownr_fn: contract.job.owner.ownr_fn,
|
||||
|
||||
@@ -6,7 +6,9 @@ import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import InputPhone from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
export default function ContractFormComponent({ form }) {
|
||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
||||
|
||||
export default function ContractFormComponent({ form, create = false }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -14,18 +16,21 @@ export default function ContractFormComponent({ form }) {
|
||||
<FormFieldsChanged form={form} />
|
||||
</div>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
@@ -50,12 +55,14 @@ export default function ContractFormComponent({ form }) {
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow>
|
||||
@@ -71,9 +78,11 @@ export default function ContractFormComponent({ form }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
@@ -291,16 +300,16 @@ export default function ContractFormComponent({ form }) {
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
||||
<InputNumber precision={2} />
|
||||
<InputNumberCalculator precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
||||
<InputNumber precision={2} />
|
||||
<InputNumberCalculator precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
||||
<InputNumber precision={2} />
|
||||
<InputNumberCalculator precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
||||
<InputNumber precision={2} />
|
||||
<InputNumberCalculator precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
|
||||
@@ -9,17 +9,17 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||
variables: {
|
||||
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
|
||||
}
|
||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
||||
},
|
||||
});
|
||||
const [selectedJob, setSelectedJob] = selectedJobState;
|
||||
|
||||
const handleSelect = record => {
|
||||
const handleSelect = (record) => {
|
||||
setSelectedJob(record.id);
|
||||
};
|
||||
|
||||
|
||||
@@ -55,14 +55,17 @@ export default function ContractsList({ loading, contracts, refetch, total }) {
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.vehicle"),
|
||||
title: t("contracts.labels.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
//sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
//sortOrder:
|
||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={`/manage/courtesycars/${record.courtesycar.id}`}
|
||||
>{`${record.courtesycar.fleetnumber} - ${record.courtesycar.year} ${record.courtesycar.make} ${record.courtesycar.model}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function CourtesyCarContractListComponent({ contracts }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
export default function CourtesyCarContractListComponent({
|
||||
contracts,
|
||||
totalContracts,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder } = search;
|
||||
const history = useHistory();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -19,79 +22,78 @@ export default function CourtesyCarContractListComponent({ contracts }) {
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||
state.sortedInfo.order,
|
||||
sortOrder: sortcolumn === "agreementnumber" && sortorder,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "job.ro_number" &&
|
||||
state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => t(record.status),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||
sortOrder: sortcolumn === "start" && sortorder,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||
state.sortedInfo.order,
|
||||
sortOrder: sortcolumn === "scheduledreturn" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
size="small"
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns.map(item => ({ ...item }))}
|
||||
scroll={{ x: true }}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
current: parseInt(page || 1),
|
||||
total: totalContracts,
|
||||
}}
|
||||
columns={columns.map((item) => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -6,161 +6,139 @@ import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status
|
||||
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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
||||
|
||||
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Button type="primary" loading={saveLoading} htmlType="submit">
|
||||
<Button
|
||||
type="primary"
|
||||
loading={saveLoading}
|
||||
onClick={() => form.submit()}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<div className="imex-flex-row__grow imex-flex-row__margin-large">
|
||||
<FormFieldsChanged form={form} />
|
||||
</div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.model")}
|
||||
name="model"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
name="year"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.plate")}
|
||||
name="plate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.color")}
|
||||
name="color"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.vin")}
|
||||
name="vin"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fleetnumber")}
|
||||
name="fleetnumber"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.purchasedate")}
|
||||
name="purchasedate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.servicestartdate")}
|
||||
name="servicestartdate"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.serviceenddate")}
|
||||
name="serviceenddate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.leaseenddate")}
|
||||
name="leaseenddate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicekm")}
|
||||
name="nextservicekm"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicedate")}
|
||||
name="nextservicedate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<FormFieldsChanged form={form} />
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.model")}
|
||||
name="model"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
name="year"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.plate")}
|
||||
name="plate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.color")}
|
||||
name="color"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.vin")}
|
||||
name="vin"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.mileage")}
|
||||
name="mileage"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fleetnumber")}
|
||||
name="fleetnumber"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.purchasedate")}
|
||||
name="purchasedate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.servicestartdate")}
|
||||
name="servicestartdate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.serviceenddate")}
|
||||
name="serviceenddate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.leaseenddate")}
|
||||
name="leaseenddate"
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name="fuel"
|
||||
@@ -173,33 +151,78 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicekm")}
|
||||
name="nextservicekm"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumberCalculator />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicedate")}
|
||||
name="nextservicedate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn")),
|
||||
});
|
||||
|
||||
export function InvoiceEnterModalContainer({
|
||||
export function BillEnterModalContainer({
|
||||
courtesyCarReturnModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
@@ -84,4 +84,4 @@ export function InvoiceEnterModalContainer({
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InvoiceEnterModalContainer);
|
||||
)(BillEnterModalContainer);
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.outwith"),
|
||||
title: t("courtesycars.labels.outwith"),
|
||||
dataIndex: "outwith",
|
||||
key: "outwith",
|
||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { Button, Upload } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { handleUpload } from "./documents-upload.utility";
|
||||
|
||||
export default function DocumentsUploadComponent({ handleUpload, UploadRef }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function DocumentsUploadComponent({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
jobId,
|
||||
tagsArray,
|
||||
billId,
|
||||
callbackAfterUpload,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Upload
|
||||
multiple={true}
|
||||
customRequest={handleUpload}
|
||||
accept="audio/*,video/*,image/*"
|
||||
ref={UploadRef}
|
||||
>
|
||||
<Button>
|
||||
<UploadOutlined /> Click to Upload
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<Upload
|
||||
multiple={true}
|
||||
customRequest={(ev) =>
|
||||
handleUpload(ev, {
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: jobId,
|
||||
billId: billId,
|
||||
tagsArray: tagsArray,
|
||||
callback: callbackAfterUpload,
|
||||
})
|
||||
}
|
||||
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||
showUploadList={false}
|
||||
>
|
||||
<Button type="primary">
|
||||
<UploadOutlined />
|
||||
</Button>
|
||||
</Upload>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(DocumentsUploadComponent);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import DocumentsUploadComponent from "./documents-upload.component";
|
||||
import { handleUpload } from "./documents-upload.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function DocumentsUploadContainer({
|
||||
jobId,
|
||||
tagsArray,
|
||||
invoiceId,
|
||||
currentUser,
|
||||
bodyshop,
|
||||
callbackAfterUpload,
|
||||
onChange,
|
||||
}) {
|
||||
return (
|
||||
<DocumentsUploadComponent
|
||||
handleUpload={(ev) =>
|
||||
handleUpload(ev, {
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: jobId,
|
||||
invoiceId: invoiceId,
|
||||
tagsArray: tagsArray,
|
||||
callback: callbackAfterUpload,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(DocumentsUploadContainer);
|
||||
@@ -1,61 +1,37 @@
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import Resizer from "react-image-file-resizer";
|
||||
import { client } from "../../App/App.container";
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import i18n from "i18next";
|
||||
import { axiosAuthInterceptorId, client } from "../../App/App.container";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
var cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
export const handleUpload = (ev, context) => {
|
||||
console.log("ev", ev);
|
||||
console.log("Handling Upload", ev);
|
||||
|
||||
logImEXEvent("document_upload", { filetype: ev.file.type });
|
||||
|
||||
const { onError, onSuccess, onProgress } = ev;
|
||||
const { bodyshop, jobId } = context;
|
||||
//If PDF, upload directly.
|
||||
//If JPEG, resize and upload.
|
||||
//TODO If this is just an invoice job? Where to put it?
|
||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name}`;
|
||||
if (ev.file.type.includes("image")) {
|
||||
Resizer.imageFileResizer(
|
||||
ev.file,
|
||||
2500,
|
||||
2500,
|
||||
"JPEG",
|
||||
75,
|
||||
0,
|
||||
(uri) => {
|
||||
let file = new File([uri], ev.file.name, {});
|
||||
file.uid = ev.file.uid;
|
||||
uploadToS3(
|
||||
key,
|
||||
file.type,
|
||||
file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context
|
||||
);
|
||||
},
|
||||
"blob"
|
||||
);
|
||||
} else {
|
||||
uploadToS3(
|
||||
key,
|
||||
ev.file.type,
|
||||
ev.file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
||||
uploadToCloudinary(
|
||||
key,
|
||||
ev.file.type,
|
||||
ev.file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context
|
||||
);
|
||||
};
|
||||
|
||||
export const uploadToS3 = (
|
||||
fileName,
|
||||
export const uploadToCloudinary = async (
|
||||
key,
|
||||
fileType,
|
||||
file,
|
||||
onError,
|
||||
@@ -63,103 +39,115 @@ export const uploadToS3 = (
|
||||
onProgress,
|
||||
context
|
||||
) => {
|
||||
const {
|
||||
bodyshop,
|
||||
jobId,
|
||||
invoiceId,
|
||||
uploaded_by,
|
||||
callback,
|
||||
tagsArray,
|
||||
} = context;
|
||||
const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray } = context;
|
||||
|
||||
//Set variables for getting the signed URL.
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
let public_id = fileName;
|
||||
let public_id = key;
|
||||
let tags = `${bodyshop.textid},${
|
||||
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
||||
}`;
|
||||
let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
axios
|
||||
.post("/media/sign", {
|
||||
eager: eager,
|
||||
public_id: public_id,
|
||||
tags: tags,
|
||||
timestamp: timestamp,
|
||||
})
|
||||
.then((response) => {
|
||||
var signature = response.data;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
onUploadProgress: (e) => {
|
||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("eager", eager);
|
||||
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
||||
formData.append("public_id", public_id);
|
||||
formData.append("tags", tags);
|
||||
formData.append("timestamp", timestamp);
|
||||
formData.append("signature", signature);
|
||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
|
||||
axios
|
||||
.post(
|
||||
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
|
||||
formData,
|
||||
options
|
||||
)
|
||||
.then((response) => {
|
||||
console.log("Upload Response", response);
|
||||
client
|
||||
.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
jobid: jobId,
|
||||
uploaded_by: uploaded_by,
|
||||
key: fileName,
|
||||
invoiceid: invoiceId,
|
||||
type: fileType,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
if (!!onSuccess)
|
||||
onSuccess({
|
||||
uid: r.data.insert_documents.returning[0].id,
|
||||
name: r.data.insert_documents.returning[0].name,
|
||||
status: "done",
|
||||
key: r.data.insert_documents.returning[0].key,
|
||||
});
|
||||
notification["success"]({
|
||||
message: i18n.t("documents.successes.insert"),
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
// if (onChange) {
|
||||
// //Used in a form.
|
||||
// onChange(UploadRef.current.state.fileList);
|
||||
// }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!!onError) onError(error);
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.getpresignurl", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
//Get the signed url.
|
||||
|
||||
const signedURLResponse = await axios.post("/media/sign", {
|
||||
public_id: public_id,
|
||||
tags: tags,
|
||||
timestamp: timestamp,
|
||||
upload_preset: "incoming_upload",
|
||||
});
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
||||
if (!!onError) onError(signedURLResponse.statusText);
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.getpresignurl", {
|
||||
message: signedURLResponse.statusText,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Build request to end to cloudinary.
|
||||
var signature = signedURLResponse.data;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
onUploadProgress: (e) => {
|
||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
console.log("Applying lower quality transforms.");
|
||||
formData.append("upload_preset", "incoming_upload");
|
||||
|
||||
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
||||
formData.append("public_id", public_id);
|
||||
formData.append("tags", tags);
|
||||
formData.append("timestamp", timestamp);
|
||||
formData.append("signature", signature);
|
||||
|
||||
//Upload request to Cloudinary
|
||||
const cloudinaryUploadResponse = await cleanAxios.post(
|
||||
`${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
|
||||
formData,
|
||||
{
|
||||
...options,
|
||||
}
|
||||
);
|
||||
console.log("Upload Response", cloudinaryUploadResponse.data);
|
||||
|
||||
if (cloudinaryUploadResponse.status !== 200) {
|
||||
console.log(
|
||||
"Error uploading to cloudinary.",
|
||||
cloudinaryUploadResponse.statusText
|
||||
);
|
||||
if (!!onError) onError(cloudinaryUploadResponse.statusText);
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: cloudinaryUploadResponse.statusText,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Insert the document with the matching key.
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
jobid: jobId,
|
||||
uploaded_by: uploaded_by,
|
||||
key: key,
|
||||
billid: billId,
|
||||
type: fileType,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!documentInsert.errors) {
|
||||
if (!!onSuccess)
|
||||
onSuccess({
|
||||
uid: documentInsert.data.insert_documents.returning[0].id,
|
||||
name: documentInsert.data.insert_documents.returning[0].name,
|
||||
status: "done",
|
||||
key: documentInsert.data.insert_documents.returning[0].key,
|
||||
});
|
||||
notification["success"]({
|
||||
message: i18n.t("documents.successes.insert"),
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
if (!!onError) onError(JSON.stringify(documentInsert.errors));
|
||||
notification["error"]({
|
||||
message: i18n.t("documents.errors.insert", {
|
||||
message: JSON.stringify(JSON.stringify(documentInsert.errors)),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
||||
value={value ? moment(value) : null}
|
||||
onChange={handleChange}
|
||||
format={dateFormat}
|
||||
onBlur={onBlur}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 }, ref) => {
|
||||
const DateTimePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
|
||||
// const handleChange = (newDate) => {
|
||||
// if (value !== newDate && onChange) {
|
||||
// onChange(newDate);
|
||||
@@ -15,13 +15,20 @@ const DateTimePicker = ({ value, onChange, onBlur }, ref) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormDatePicker value={value} onChange={onChange} />
|
||||
<FormDatePicker
|
||||
{...restProps}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<TimePicker
|
||||
{...restProps}
|
||||
value={value ? moment(value) : null}
|
||||
onChange={onChange}
|
||||
showSecond={false}
|
||||
minuteStep={15}
|
||||
onBlur={onBlur}
|
||||
format="hh:mm a"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import { Input, Popover } from "antd";
|
||||
import React, { useEffect, useState, forwardRef } from "react";
|
||||
import { InputNumber, Popover } from "antd";
|
||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
||||
|
||||
const FormInputNUmberCalculator = (
|
||||
{ value: formValue, onChange: formOnChange },
|
||||
ref
|
||||
{ value: formValue, onChange: formOnChange, ...restProps },
|
||||
refProp
|
||||
) => {
|
||||
const [value, setValue] = useState(formValue);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [history, setHistory] = useState([]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const key = e.target.value;
|
||||
switch (key) {
|
||||
case "/":
|
||||
case "*":
|
||||
case "+":
|
||||
case "-":
|
||||
return;
|
||||
default:
|
||||
setValue(key);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const ref = useRef(null);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
console.log("e :>> ", e.currentTarget.value);
|
||||
const { key } = e;
|
||||
|
||||
let action;
|
||||
switch (key) {
|
||||
case "/":
|
||||
@@ -38,11 +26,13 @@ const FormInputNUmberCalculator = (
|
||||
action = "=";
|
||||
break;
|
||||
default:
|
||||
setValue(e.currentTarget.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const val = parseFloat(value);
|
||||
setValue(null);
|
||||
ref.current.blur();
|
||||
ref.current.focus();
|
||||
if (!isNaN(val)) {
|
||||
setHistory([...history, val, action]);
|
||||
}
|
||||
@@ -82,7 +72,12 @@ const FormInputNUmberCalculator = (
|
||||
}
|
||||
}, 0);
|
||||
setTotal(subTotal);
|
||||
if (history[history.length - 1] === "=") setValue(subTotal);
|
||||
if (history[history.length - 1] === "=") {
|
||||
setValue(subTotal);
|
||||
ref.current.blur();
|
||||
ref.current.focus();
|
||||
setHistory([]);
|
||||
}
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
@@ -101,13 +96,13 @@ const FormInputNUmberCalculator = (
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} visible={history.length > 0}>
|
||||
<Input
|
||||
<InputNumber
|
||||
ref={ref}
|
||||
value={value}
|
||||
defaultValue={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={(e) => setHistory([])}
|
||||
{...restProps}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { BlockPicker } from "react-color";
|
||||
//To be used as a form element only.
|
||||
|
||||
const ColorPickerFormItem = ({ value, onChange, style, ...restProps }, ref) => {
|
||||
const handleChangeComplete = (color) => {
|
||||
if (onChange) onChange(color);
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPicker
|
||||
{...restProps}
|
||||
style={{ width: "100%", ...style }}
|
||||
color={value}
|
||||
triangle="hide"
|
||||
onChangeComplete={handleChangeComplete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default forwardRef(ColorPickerFormItem);
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LaborTypeFormItem = ({ value, onChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
return <div>{t(`joblines.fields.lbr_types.${value}`)}</div>;
|
||||
};
|
||||
export default forwardRef(LaborTypeFormItem);
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PartTypeFormItem = ({ value, onChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
return <div>{t(`joblines.fields.part_types.${value}`)}</div>;
|
||||
};
|
||||
export default forwardRef(PartTypeFormItem);
|
||||
@@ -0,0 +1,17 @@
|
||||
import Dinero from "dinero.js";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
|
||||
if (!value) return null;
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <div>{value}</div>;
|
||||
case "currency":
|
||||
return (
|
||||
<div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>
|
||||
);
|
||||
default:
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
};
|
||||
export default forwardRef(ReadOnlyFormItem);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Button } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
function ResetForm({ resetFields }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<AlertComponent
|
||||
message={
|
||||
<div>
|
||||
{t("general.messages.unsavedchanges")}
|
||||
<Button style={{ marginLeft: "20px" }} onClick={() => resetFields()}>
|
||||
{t("general.actions.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
closable
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(ResetForm);
|
||||
@@ -132,16 +132,16 @@ export default function GlobalSearch() {
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.invoices")),
|
||||
options: data.search_invoices.map((invoice) => {
|
||||
label: renderTitle(t("menus.header.search.bills")),
|
||||
options: data.search_bills.map((bill) => {
|
||||
return {
|
||||
value: `${invoice.invoice_number}`,
|
||||
value: `${bill.invoice_number}`,
|
||||
label: (
|
||||
<Link to={`/manage/invoices?invoiceid=${invoice.id}`}>
|
||||
<Link to={`/manage/bills?billid=${bill.id}`}>
|
||||
<div className="imex-flex-row">
|
||||
<span className="imex-flex-row__margin-large">{`${invoice.invoice_number}`}</span>
|
||||
<span className="imex-flex-row__margin-large">{`${invoice.vendor.name}`}</span>
|
||||
<span className="imex-flex-row__margin-large">{`${invoice.date}`}</span>
|
||||
<span className="imex-flex-row__margin-large">{`${bill.invoice_number}`}</span>
|
||||
<span className="imex-flex-row__margin-large">{`${bill.vendor.name}`}</span>
|
||||
<span className="imex-flex-row__margin-large">{`${bill.date}`}</span>
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
|
||||
@@ -12,8 +12,9 @@ import Icon, {
|
||||
TeamOutlined,
|
||||
UnorderedListOutlined,
|
||||
UserOutlined,
|
||||
ToolFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { Avatar, Layout, Menu } from "antd";
|
||||
import { Avatar, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
@@ -26,25 +27,25 @@ import {
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectRecentItems } from "../../redux/application/application.selectors";
|
||||
import {
|
||||
selectRecentItems,
|
||||
selectSelectedHeader,
|
||||
} from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import "./header.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
recentItems: selectRecentItems,
|
||||
selectedHeader: selectSelectedHeader,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setInvoiceEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setPaymentContext: (context) =>
|
||||
@@ -53,291 +54,292 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
function Header({
|
||||
bodyshop,
|
||||
handleMenuClick,
|
||||
currentUser,
|
||||
selectedHeader,
|
||||
signOutStart,
|
||||
setInvoiceEnterContext,
|
||||
setBillEnterContext,
|
||||
setTimeTicketContext,
|
||||
setPaymentContext,
|
||||
recentItems,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { Header } = Layout;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
theme="dark"
|
||||
className="header-main-menu"
|
||||
selectedKeys={["home"]}
|
||||
onClick={handleMenuClick}
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="home">
|
||||
<Link to="/manage">
|
||||
<HomeFilled />
|
||||
{t("menus.header.home")}
|
||||
<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")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="schedule">
|
||||
<Link to="/manage/schedule">
|
||||
<Icon component={FaCalendarAlt} />
|
||||
{t("menus.header.schedule")}
|
||||
<Menu.Item key="availablejobs">
|
||||
<Link to="/manage/available">
|
||||
<ImportOutlined /> {t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<Icon component={FaCarCrash} />
|
||||
<span>{t("menus.header.jobs")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="activejobs">
|
||||
<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 />
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
</Menu.Item>
|
||||
<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
|
||||
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="invoices">
|
||||
<Link to="/manage/invoices">{t("menus.header.invoices")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterinvoices"
|
||||
onClick={() => {
|
||||
setInvoiceEnterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon component={FaFileInvoiceDollar} />
|
||||
{t("menus.header.enterinvoices")}
|
||||
</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 />
|
||||
{t("menus.header.courtesycars-contracts")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</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={
|
||||
<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>
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
<span>{t("menus.currentuser.languageselector")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
<Menu.Item actiontype="lang-select" key="en-US">
|
||||
{t("general.languages.english")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shiftclock">
|
||||
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
<Menu.Item actiontype="lang-select" key="fr-CA">
|
||||
{t("general.languages.french")}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
<Menu.Item actiontype="lang-select" key="es-MX">
|
||||
{t("general.languages.spanish")}
|
||||
</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>
|
||||
</Header>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
max-height: 3.5rem;
|
||||
}
|
||||
.header-main-menu {
|
||||
width: 80vw;
|
||||
float: left;
|
||||
//width: 95vw;
|
||||
//float: left;
|
||||
}
|
||||
|
||||
53
client/src/components/help-rescue/help-rescue.component.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Button, Card, Input, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function HelpRescue() {
|
||||
const { t } = useTranslation();
|
||||
const [code, setCode] = useState("");
|
||||
|
||||
const handleClick = async () => {
|
||||
var bodyFormData = new FormData();
|
||||
bodyFormData.append("Code", code);
|
||||
|
||||
const res1 = await fetch(
|
||||
"https://secure.logmeinrescue.com/Customer/Code.aspx",
|
||||
{
|
||||
method: "POST",
|
||||
body: bodyFormData,
|
||||
}
|
||||
);
|
||||
console.log("handleClick -> res1", res1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("help.labels.rescuetitle")}>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<Space direction="vertical" align="center">
|
||||
<div>{t("help.labels.rescuedesc")}</div>
|
||||
<form
|
||||
name="logmeinsupport"
|
||||
action="https://secure.logmeinrescue.com/Customer/Code.aspx"
|
||||
method="post"
|
||||
>
|
||||
<span>
|
||||
Enter your six-digit code, then click the Start Download button
|
||||
below{" "}
|
||||
</span>
|
||||
<input type="text" name="Code" />
|
||||
<br />
|
||||
<input type="submit" value="Connect to technician" />
|
||||
</form>
|
||||
<Input
|
||||
size="large"
|
||||
style={{ width: "10rem" }}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
value={code}
|
||||
placeholder={t("help.labels.codeplacholder")}
|
||||
/>
|
||||
<Button onClick={handleClick}>{t("help.actions.connect")}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||
import { Form, Button } from "antd";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_INVOICE_BY_PK,
|
||||
UPDATE_INVOICE,
|
||||
} from "../../graphql/invoices.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import InvoiceFormContainer from "../invoice-form/invoice-form.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { UPDATE_INVOICE_LINE } from "../../graphql/invoice-lines.queries";
|
||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function InvoiceDetailEditContainer({ bodyshop }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [updateInvoice] = useMutation(UPDATE_INVOICE);
|
||||
const [updateInvoiceLine] = useMutation(UPDATE_INVOICE_LINE);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_INVOICE_BY_PK, {
|
||||
variables: { invoiceid: search.invoiceid },
|
||||
skip: !!!search.invoiceid,
|
||||
});
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
const { invoicelines, upload, ...invoice } = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
updateInvoice({
|
||||
variables: { invoiceId: search.invoiceid, invoice: invoice },
|
||||
})
|
||||
);
|
||||
|
||||
invoicelines.forEach((il) => {
|
||||
delete il.__typename;
|
||||
updates.push(
|
||||
updateInvoiceLine({
|
||||
variables: {
|
||||
invoicelineId: il.id,
|
||||
invoiceLine: {
|
||||
...il,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (search.invoiceid) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, search.invoiceid]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (!!!search.invoiceid)
|
||||
return <div>{t("invoices.labels.noneselected")}</div>;
|
||||
return (
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={
|
||||
data
|
||||
? {
|
||||
...data.invoices_by_pk,
|
||||
|
||||
invoicelines: data.invoices_by_pk.invoicelines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
||||
false,
|
||||
state:
|
||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
||||
false,
|
||||
local:
|
||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
||||
false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.invoices_by_pk
|
||||
? moment(data.invoices_by_pk.date)
|
||||
: null,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Button htmlType="submit" loading={updateLoading} type="primary">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<InvoiceFormContainer form={form} invoiceEdit />
|
||||
<JobDocumentsGallery
|
||||
jobId={data ? data.invoices_by_pk.jobid : null}
|
||||
invoiceId={search.invoiceid}
|
||||
documentsList={data ? data.invoices_by_pk.documents : []}
|
||||
invoicesCallback={refetch}
|
||||
/>
|
||||
</Form>
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(InvoiceDetailEditContainer);
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useLazyQuery, useQuery } from "@apollo/react-hooks";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
|
||||
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
|
||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InvoiceFormComponent from "./invoice-form.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function InvoiceFormContainer({ bodyshop, form, invoiceEdit }) {
|
||||
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
|
||||
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
|
||||
});
|
||||
|
||||
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
|
||||
|
||||
const [loadLines, { data: lineData }] = useLazyQuery(
|
||||
GET_JOB_LINES_TO_ENTER_INVOICE
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InvoiceFormComponent
|
||||
form={form}
|
||||
invoiceEdit={invoiceEdit}
|
||||
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
|
||||
vendorAutoCompleteOptions={
|
||||
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
||||
}
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(InvoiceFormContainer);
|
||||
@@ -1,224 +0,0 @@
|
||||
import { DeleteFilled, WarningOutlined } from "@ant-design/icons";
|
||||
import { Button, Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import InvoiceLineSearchSelect from "../invoice-line-search-select/invoice-line-search-select.component";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
export default function InvoiceEnterModalLinesComponent({
|
||||
lineData,
|
||||
discount,
|
||||
form,
|
||||
responsibilityCenters,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { setFieldsValue, getFieldsValue } = form;
|
||||
|
||||
return (
|
||||
<Form.List name="invoicelines">
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div className="invoice-form-lines-wrapper">
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item required={false} key={field.key}>
|
||||
<div className="invoice-form-line">
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.jobline")}
|
||||
key={`${index}joblinename`}
|
||||
name={[field.name, "joblineid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InvoiceLineSearchSelect
|
||||
options={lineData}
|
||||
onSelect={(value, opt) => {
|
||||
setFieldsValue({
|
||||
invoicelines: getFieldsValue([
|
||||
"invoicelines",
|
||||
]).invoicelines.map((item, idx) => {
|
||||
if (idx === index) {
|
||||
return {
|
||||
...item,
|
||||
line_desc: opt.line_desc,
|
||||
quantity: opt.part_qty || 1,
|
||||
actual_price: opt.cost,
|
||||
cost_center: opt.part_type
|
||||
? responsibilityCenters.defaults.costs[
|
||||
opt.part_type
|
||||
] || null
|
||||
: null,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.actual")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onBlur={(e) => {
|
||||
setFieldsValue({
|
||||
invoicelines: getFieldsValue(
|
||||
"invoicelines"
|
||||
).invoicelines.map((item, idx) => {
|
||||
if (idx === index) {
|
||||
return {
|
||||
...item,
|
||||
actual_cost: !!item.actual_cost
|
||||
? item.actual_cost
|
||||
: parseFloat(e.target.value) * (1 - discount),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.actual_cost")}
|
||||
key={`${index}actual_cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const line = getFieldsValue(["invoicelines"])
|
||||
.invoicelines[index];
|
||||
if (!!!line) return null;
|
||||
const lineDiscount = (
|
||||
1 -
|
||||
Math.round(
|
||||
(line.actual_cost / line.actual_price) * 100
|
||||
) /
|
||||
100
|
||||
).toPrecision(2);
|
||||
|
||||
if (lineDiscount - discount === 0) return null;
|
||||
return <WarningOutlined style={{ color: "red" }} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.cost_center")}
|
||||
key={`${index}cost_center`}
|
||||
name={[field.name, "cost_center"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select style={{ width: "150px" }}>
|
||||
{responsibilityCenters.costs.map((item) => (
|
||||
<Select.Option key={item.name}>
|
||||
{item.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.federal_tax_applicable")}
|
||||
key={`${index}fedtax`}
|
||||
initialValue={true}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "federal"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.state_tax_applicable")}
|
||||
key={`${index}statetax`}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "state"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.local_tax_applicable")}
|
||||
key={`${index}localtax`}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "local"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "50%" }}
|
||||
>
|
||||
{t("invoicelines.actions.newline")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
.invoice-form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.invoice-form-totals {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-form-invoice-details {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-form-lines-wrapper {
|
||||
border: 3px ridge rgba(28, 110, 164, 0.24);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.invoice-form-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
border-bottom: 2px dashed rgba(7, 7, 7, 0.4);
|
||||
F & > * {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
//DEPRECATED.
|
||||
|
||||
// import React, { useState } from "react";
|
||||
// import { QUERY_INVOICES_BY_VENDOR_PAGINATED } from "../../graphql/invoices.queries";
|
||||
// import { useQuery } from "@apollo/react-hooks";
|
||||
// import queryString from "query-string";
|
||||
// import { useHistory, useLocation } from "react-router-dom";
|
||||
// import { Table, Input } from "antd";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { alphaSort } from "../../utils/sorters";
|
||||
// import AlertComponent from "../alert/alert.component";
|
||||
// import { DateFormatter } from "../../utils/DateFormatter";
|
||||
// import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
|
||||
// export default function InvoicesByVendorList() {
|
||||
// const search = queryString.parse(useLocation().search);
|
||||
// const history = useHistory();
|
||||
// const { page, sortcolumn, sortorder } = search;
|
||||
|
||||
// const { loading, error, data } = useQuery(
|
||||
// QUERY_INVOICES_BY_VENDOR_PAGINATED,
|
||||
// {
|
||||
// variables: {
|
||||
// vendorId: search.vendorid,
|
||||
// offset: page ? (page - 1) * 25 : 0,
|
||||
// limit: 25,
|
||||
// order: [
|
||||
// {
|
||||
// [sortcolumn || "date"]: sortorder
|
||||
// ? sortorder === "descend"
|
||||
// ? "desc"
|
||||
// : "asc"
|
||||
// : "desc",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// skip: !!!search.vendorid,
|
||||
// }
|
||||
// );
|
||||
|
||||
// const { t } = useTranslation();
|
||||
|
||||
// const [state, setState] = useState({
|
||||
// sortedInfo: {},
|
||||
// search: "",
|
||||
// });
|
||||
|
||||
// const handleTableChange = (pagination, filters, sorter) => {
|
||||
// setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
// search.page = pagination.current;
|
||||
// search.sortcolumn = sorter.columnKey;
|
||||
// search.sortorder = sorter.order;
|
||||
// history.push({ search: queryString.stringify(search) });
|
||||
// };
|
||||
|
||||
// const handleOnRowClick = (record) => {
|
||||
// if (record) {
|
||||
// if (record.id) {
|
||||
// search.invoiceid = record.id;
|
||||
// history.push({ search: queryString.stringify(search) });
|
||||
// }
|
||||
// } else {
|
||||
// delete search.invoiceid;
|
||||
// history.push({ search: queryString.stringify(search) });
|
||||
// }
|
||||
// };
|
||||
|
||||
// const columns = [
|
||||
// {
|
||||
// title: t("invoices.fields.invoice_number"),
|
||||
// dataIndex: "invoice_number",
|
||||
// key: "invoice_number",
|
||||
// sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "invoice_number" &&
|
||||
// state.sortedInfo.order,
|
||||
// },
|
||||
// {
|
||||
// title: t("invoices.fields.date"),
|
||||
// dataIndex: "date",
|
||||
// key: "date",
|
||||
|
||||
// sorter: (a, b) => a.date - b.date,
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
// render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
// },
|
||||
// {
|
||||
// title: t("invoices.fields.total"),
|
||||
// dataIndex: "total",
|
||||
// key: "total",
|
||||
|
||||
// sorter: (a, b) => a.total - b.total,
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
// render: (text, record) => (
|
||||
// <CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
// ),
|
||||
// },
|
||||
// ];
|
||||
|
||||
// const handleSearch = (e) => {
|
||||
// setState({ ...state, search: e.target.value });
|
||||
// };
|
||||
|
||||
// const dataSource = state.search
|
||||
// ? data.invoices.filter(
|
||||
// (i) =>
|
||||
// (i.invoice_number || "")
|
||||
// .toLowerCase()
|
||||
// .includes(state.search.toLowerCase()) ||
|
||||
// (i.amount || "").toString().includes(state.search)
|
||||
// )
|
||||
// : (data && data.invoices) || [];
|
||||
|
||||
// if (error) return <AlertComponent message={error.message} type='error' />;
|
||||
|
||||
// return (
|
||||
// <Table
|
||||
// loading={loading}
|
||||
// title={() => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Input
|
||||
// value={state.search}
|
||||
// onChange={handleSearch}
|
||||
// placeholder={t("general.labels.search")}
|
||||
// allowClear
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }}
|
||||
// dataSource={dataSource}
|
||||
// size='small'
|
||||
// scroll={{ x: true }}
|
||||
// pagination={{
|
||||
// position: "top",
|
||||
// pageSize: 25,
|
||||
// current: parseInt(page || 1),
|
||||
// total: data ? data.invoices_aggregate.aggregate.count : 0,
|
||||
// }}
|
||||
// columns={columns}
|
||||
// rowKey='id'
|
||||
// onChange={handleTableChange}
|
||||
// rowSelection={{
|
||||
// onSelect: (record) => {
|
||||
// handleOnRowClick(record);
|
||||
// },
|
||||
// selectedRowKeys: [search.invoiceid],
|
||||
// type: "radio",
|
||||
// }}
|
||||
// onRow={(record, rowIndex) => {
|
||||
// return {
|
||||
// onClick: (event) => {
|
||||
// handleOnRowClick(record);
|
||||
// }, // click row
|
||||
// };
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export default function JiraSupportComponent() {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
|
||||
script.src = "https://jsd-widget.atlassian.com/assets/embed.js";
|
||||
// script.attributes.setNamedItem({ "data-jsd-embedded": true });
|
||||
// script.attributes.setNamedItem({
|
||||
// "data-key": "d69bb65c-1dd3-483f-b109-66a970d03f44",
|
||||
// });
|
||||
// script.attributes.setNamedItem({
|
||||
// "data-base-url": "https://jsd-widget.atlassian.com",
|
||||
// });
|
||||
|
||||
// script["data-jsd-embedded"] = true;
|
||||
// script["data-key"] = "d69bb65c-1dd3-483f-b109-66a970d03f44";
|
||||
// script["data-base-url"] = "https://jsd-widget.atlassian.com";
|
||||
script.async = true;
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div>JIra</div>;
|
||||
}
|
||||
@@ -4,23 +4,23 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import "./job-invoices-total.styles.scss";
|
||||
import "./job-bills-total.styles.scss";
|
||||
|
||||
export default function JobInvoiceTotals({ loading, invoices, jobTotals }) {
|
||||
export default function JobBillsTotalComponent({ loading, bills, jobTotals }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!!!jobTotals)
|
||||
return (
|
||||
<AlertComponent type='error' message={t("jobs.errors.nofinancial")} />
|
||||
<AlertComponent type="error" message={t("jobs.errors.nofinancial")} />
|
||||
);
|
||||
|
||||
const totals = JSON.parse(jobTotals);
|
||||
|
||||
let invoiceTotals = Dinero({ amount: 0 });
|
||||
invoices.forEach((i) =>
|
||||
i.invoicelines.forEach((il) => {
|
||||
invoiceTotals = invoiceTotals.add(
|
||||
const totals = jobTotals;
|
||||
|
||||
let billTotals = Dinero({ amount: 0 });
|
||||
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
|
||||
@@ -30,10 +30,10 @@ export default function JobInvoiceTotals({ loading, invoices, jobTotals }) {
|
||||
})
|
||||
);
|
||||
|
||||
const discrepancy = Dinero(totals.parts.parts.total).subtract(invoiceTotals);
|
||||
const discrepancy = Dinero(totals.parts.parts.total).subtract(billTotals);
|
||||
|
||||
return (
|
||||
<div className='job-invoices-totals-container'>
|
||||
<div className="job-bills-totals-container">
|
||||
<Statistic
|
||||
title={t("jobs.labels.partstotal")}
|
||||
value={Dinero(totals.parts.parts.total).toFormat()}
|
||||
@@ -43,11 +43,11 @@ export default function JobInvoiceTotals({ loading, invoices, jobTotals }) {
|
||||
value={Dinero(totals.parts.sublets.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.retailtotal")}
|
||||
value={invoiceTotals.toFormat()}
|
||||
title={t("bills.labels.retailtotal")}
|
||||
value={billTotals.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.discrepancy")}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color: discrepancy.getAmount === 0 ? "green" : "red",
|
||||
}}
|
||||
@@ -1,4 +1,4 @@
|
||||
.job-invoices-totals-container {
|
||||
.job-bills-totals-container {
|
||||
margin: 0rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3,16 +3,9 @@ import Axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function JobCalculateTotals({ bodyshop, job }) {
|
||||
export default function JobCalculateTotals({ job, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
@@ -22,7 +15,6 @@ export function JobCalculateTotals({ bodyshop, job }) {
|
||||
const newTotals = (
|
||||
await Axios.post("/job/totals", {
|
||||
job: job,
|
||||
shoprates: bodyshop.shoprates,
|
||||
})
|
||||
).data;
|
||||
|
||||
@@ -50,10 +42,9 @@ export function JobCalculateTotals({ bodyshop, job }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button loading={loading} onClick={handleCalculate}>
|
||||
<Button loading={loading} onClick={handleCalculate} disabled={disabled}>
|
||||
{t("jobs.actions.recalculate")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(JobCalculateTotals);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { Button, Form, notification, Switch } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation, useParams } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||
import { MARK_LATEST_APPOINTMENT_AS_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../../../redux/user/user.selectors";
|
||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobChecklistForm({
|
||||
formItems,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
type,
|
||||
job,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [intakeJob] = useMutation(UPDATE_JOB);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
|
||||
const { jobId } = useParams();
|
||||
const history = useHistory();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("job_complete_intake");
|
||||
|
||||
const result = await intakeJob({
|
||||
variables: {
|
||||
jobId: jobId,
|
||||
job: {
|
||||
...(type === "intake" && { inproduction: values.addToProduction }),
|
||||
status:
|
||||
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered),
|
||||
...(type === "intake" && { actual_in: new Date() }),
|
||||
|
||||
...(type === "intake" && {
|
||||
scheduled_completion: values.scheduled_completion,
|
||||
}),
|
||||
|
||||
[(type === "intake" && "intakechecklist") ||
|
||||
(type === "deliver" && "deliverchecklist")]: {
|
||||
...values,
|
||||
completed_by: currentUser.email,
|
||||
completed_at: new Date(),
|
||||
},
|
||||
|
||||
...(type === "deliver" && {
|
||||
scheduled_delivery: values.scheduled_delivery,
|
||||
}),
|
||||
...(type === "deliver" &&
|
||||
values.removeFromProduction && {
|
||||
inproduction: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!!search.appointmentId) {
|
||||
const appUpdate = await markAptArrived({
|
||||
variables: { appointmentId: search.appointmentId },
|
||||
});
|
||||
|
||||
if (!!appUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("checklist.errors.complete", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("checklist.successes.completed") });
|
||||
history.push(`/manage/jobs/${jobId}`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("checklist.errors.complete", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
...(type === "intake" && {
|
||||
addToProduction: true,
|
||||
scheduled_completion: job && job.scheduled_completion,
|
||||
scheduled_delivery: job && job.scheduled_delivery,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{t("checklist.labels.checklist")}
|
||||
|
||||
<ConfigFormComponents componentList={formItems} />
|
||||
|
||||
{type === "intake" && (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="addToProduction"
|
||||
valuePropName="checked"
|
||||
label={t("checklist.labels.addtoproduction")}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduled_completion"
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduled_delivery"
|
||||
label={t("jobs.fields.scheduled_delivery")}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
{type === "deliver" && (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="actualCompletion"
|
||||
label={t("jobs.fields.actual_completion")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="removeFromProduction"
|
||||
valuePropName="checked"
|
||||
label={t("checklist.labels.removefromproduction")}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button loading={loading} htmlType="submit">
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobChecklistForm);
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobChecklistTemplateItem({
|
||||
templateKey,
|
||||
renderTemplate,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{t(`printcenter.jobs.${templateKey}`)}
|
||||
<PrinterFilled onClick={() => renderTemplate(templateKey)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import JobIntakeTemplateItem from "../job-intake-template-item/job-intake-template-item.component";
|
||||
import JobIntakeTemplateItem from "../job-checklist-template-item/job-checklist-template-item.component";
|
||||
import { useParams } from "react-router-dom";
|
||||
import RenderTemplate, {
|
||||
displayTemplateInWindow,
|
||||
@@ -22,9 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export function JobIntakeTemplateList({ bodyshop, templates }) {
|
||||
const { jobId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
//TODO SHould this be using the generic one?
|
||||
const renderTemplate = async (templateKey) => {
|
||||
logImEXEvent("job_intake_template_render");
|
||||
logImEXEvent("job_checklist_template_render");
|
||||
|
||||
const html = await RenderTemplate(
|
||||
{
|
||||
@@ -37,7 +36,7 @@ export function JobIntakeTemplateList({ bodyshop, templates }) {
|
||||
};
|
||||
|
||||
const renderAllTemplates = () => {
|
||||
logImEXEvent("job_intake_render_all_templates");
|
||||
logImEXEvent("job_checklist_render_all_templates");
|
||||
|
||||
templates.forEach((template) => renderTemplate(template));
|
||||
};
|
||||
@@ -46,7 +45,7 @@ export function JobIntakeTemplateList({ bodyshop, templates }) {
|
||||
<div>
|
||||
{t("intake.labels.printpack")}
|
||||
<Button onClick={renderAllTemplates}>
|
||||
{t("intake.actions.printall")}
|
||||
{t("checklist.actions.printall")}
|
||||
</Button>
|
||||
{templates.map((template) => (
|
||||
<JobIntakeTemplateItem
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
|
||||
export default function JobChecklistDisplay({ checklist }) {
|
||||
console.log("JobChecklistDisplay -> checklist", checklist);
|
||||
if (!checklist) return <div></div>;
|
||||
return (
|
||||
<div>
|
||||
<ConfigFormComponents readOnly componentList={checklist} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import JobChecklistTemplateList from "./components/job-checklist-template-list/job-checklist-template-list.component";
|
||||
import JobChecklistForm from "./components/job-checklist-form/job-checklist-form.component";
|
||||
import { Row, Col } from "antd";
|
||||
export default function JobIntakeComponent({ checklistConfig, type, job }) {
|
||||
const { form, templates } = checklistConfig;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<JobChecklistTemplateList templates={templates} type={type} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<JobChecklistForm formItems={form} type={type} job={job} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -51,28 +51,25 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
{ parts: {}, labor: {} }
|
||||
);
|
||||
|
||||
const invoiceTotalsByProfitCenter = job.invoices.reduce(
|
||||
(inv_acc, inv_val) => {
|
||||
//At the invoice level.
|
||||
inv_val.invoicelines.map((line_val) => {
|
||||
//At the invoice line level.
|
||||
//console.log("JobCostingPartsTable -> line_val", line_val);
|
||||
if (!!!inv_acc[line_val.cost_center])
|
||||
inv_acc[line_val.cost_center] = Dinero();
|
||||
const billTotalsByProfitCenter = job.bills.reduce((bill_acc, bill_val) => {
|
||||
//At the invoice level.
|
||||
bill_val.billlines.map((line_val) => {
|
||||
//At the invoice line level.
|
||||
//console.log("JobCostingPartsTable -> line_val", line_val);
|
||||
if (!!!bill_acc[line_val.cost_center])
|
||||
bill_acc[line_val.cost_center] = Dinero();
|
||||
|
||||
inv_acc[line_val.cost_center] = inv_acc[line_val.cost_center].add(
|
||||
Dinero({
|
||||
amount: Math.round((line_val.actual_cost || 0) * 100),
|
||||
})
|
||||
.multiply(line_val.quantity)
|
||||
.multiply(inv_val.is_credit_memo ? -1 : 1)
|
||||
);
|
||||
return null;
|
||||
});
|
||||
return inv_acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
|
||||
Dinero({
|
||||
amount: Math.round((line_val.actual_cost || 0) * 100),
|
||||
})
|
||||
.multiply(line_val.quantity)
|
||||
.multiply(bill_val.is_credit_memo ? -1 : 1)
|
||||
);
|
||||
return null;
|
||||
});
|
||||
return bill_acc;
|
||||
}, {});
|
||||
|
||||
const ticketTotalsByProfitCenter = job.timetickets.reduce(
|
||||
(ticket_acc, ticket_val) => {
|
||||
@@ -114,12 +111,11 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
||||
|
||||
const cost_labor =
|
||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||
const cost_parts =
|
||||
invoiceTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
|
||||
|
||||
const cost = (
|
||||
invoiceTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
||||
).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
|
||||
const cost = (billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })).add(
|
||||
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
|
||||
);
|
||||
const totalSales = sale_labor.add(sale_parts);
|
||||
const gpdollars = totalSales.subtract(cost);
|
||||
const gppercent = (
|
||||
|
||||
@@ -145,7 +145,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Col {...colBreakPoints}>
|
||||
<JobDetailCardsPartsComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
@@ -163,7 +163,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Col {...colBreakPoints}>
|
||||
<JobDetailCardsDamageComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
|
||||
@@ -1,141 +1,17 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pie, PieChart, Sector } from "recharts";
|
||||
import PartsStatusPie from "../parts-status-pie/parts-status-pie.component";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
|
||||
export default function JobDetailCardsPartsComponent({ loading, data }) {
|
||||
const { t } = useTranslation();
|
||||
const { joblines_status } = data;
|
||||
// console.log(
|
||||
// "JobDetailCardsPartsComponent -> joblines_stats",
|
||||
// joblines_status
|
||||
// );
|
||||
|
||||
const memoizedData = useMemo(() => Calculatedata(joblines_status), [
|
||||
joblines_status,
|
||||
]);
|
||||
|
||||
const [state, setState] = useState({ activeIndex: 0 });
|
||||
|
||||
const onPieEnter = (data, index) => {
|
||||
setState({
|
||||
activeIndex: index,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||
<PieChart width={400} height={400}>
|
||||
<Pie
|
||||
activeIndex={state.activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
data={memoizedData}
|
||||
cx={200}
|
||||
cy={200}
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={onPieEnter}
|
||||
/>
|
||||
</PieChart>
|
||||
<PartsStatusPie joblines_status={joblines_status} />
|
||||
</CardTemplate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Calculatedata = (data) => {
|
||||
if (data.length > 0) {
|
||||
const statusMapping = {};
|
||||
data.map((i) => {
|
||||
if (!statusMapping[i.status])
|
||||
statusMapping[i.status] = { name: i.status || "No Status*", value: 0 };
|
||||
statusMapping[i.status].value = statusMapping[i.status].value + i.count;
|
||||
return null;
|
||||
});
|
||||
return Object.keys(statusMapping).map((key) => {
|
||||
return statusMapping[key];
|
||||
});
|
||||
} else {
|
||||
return [
|
||||
{ name: "Group A", value: 400 },
|
||||
{ name: "Group B", value: 300 },
|
||||
{ name: "Group C", value: 300 },
|
||||
{ name: "Group D", value: 200 },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
percent,
|
||||
value,
|
||||
} = props;
|
||||
const sin = Math.sin(-RADIAN * midAngle);
|
||||
const cos = Math.cos(-RADIAN * midAngle);
|
||||
const sx = cx + (outerRadius + 10) * cos;
|
||||
const sy = cy + (outerRadius + 10) * sin;
|
||||
const mx = cx + (outerRadius + 30) * cos;
|
||||
const my = cy + (outerRadius + 30) * sin;
|
||||
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
const ey = my;
|
||||
const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
<path
|
||||
d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
|
||||
stroke={fill}
|
||||
fill="none"
|
||||
/>
|
||||
<circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
|
||||
<text
|
||||
x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
y={ey}
|
||||
textAnchor={textAnchor}
|
||||
fill="#333"
|
||||
>{`Count: ${value}`}</text>
|
||||
<text
|
||||
x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
y={ey}
|
||||
dy={18}
|
||||
textAnchor={textAnchor}
|
||||
fill="#999"
|
||||
>
|
||||
{`(${(percent * 100).toFixed(2)}%)`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,22 +6,15 @@ import CardTemplate from "./job-detail-cards.template.component";
|
||||
|
||||
export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
||||
const { t } = useTranslation();
|
||||
let totals;
|
||||
|
||||
try {
|
||||
totals = JSON.parse(data.job_totals);
|
||||
} catch (error) {
|
||||
console.log("Error in CardsTotal component", error);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.totals")}>
|
||||
{totals ? (
|
||||
{data.job_totals ? (
|
||||
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||
<Statistic
|
||||
className="imex-flex-row__margin-large"
|
||||
title={t("jobs.labels.total_repairs")}
|
||||
value={Dinero(totals.totals.total_repairs).toFormat()}
|
||||
value={Dinero(data.job_totals.totals.total_repairs).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
className="imex-flex-row__margin-large"
|
||||
@@ -33,7 +26,7 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
||||
<Statistic
|
||||
className="imex-flex-row__margin-large"
|
||||
title={t("jobs.labels.net_repairs")}
|
||||
value={Dinero(totals.totals.net_repairs).toFormat()}
|
||||
value={Dinero(data.job_totals.totals.net_repairs).toFormat()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -15,6 +15,13 @@ import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.compone
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJobLineEditContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||
@@ -23,6 +30,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
export function JobLinesComponent({
|
||||
jobRO,
|
||||
setPartsOrderContext,
|
||||
loading,
|
||||
refetch,
|
||||
@@ -30,8 +38,9 @@ export function JobLinesComponent({
|
||||
setSearchText,
|
||||
selectedLines,
|
||||
setSelectedLines,
|
||||
jobId,
|
||||
job,
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
@@ -139,20 +148,20 @@ export function JobLinesComponent({
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty",
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>
|
||||
{record.act_price * record.part_qty}
|
||||
</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: t("joblines.fields.total"),
|
||||
// dataIndex: "total",
|
||||
// key: "total",
|
||||
// sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
// ellipsis: true,
|
||||
// render: (text, record) => (
|
||||
// <CurrencyFormatter>
|
||||
// {record.act_price * record.part_qty}
|
||||
// </CurrencyFormatter>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: t("joblines.fields.mod_lbr_ty"),
|
||||
dataIndex: "mod_lbr_ty",
|
||||
@@ -179,13 +188,17 @@ export function JobLinesComponent({
|
||||
title: t("joblines.fields.notes"),
|
||||
dataIndex: "notes",
|
||||
key: "notes",
|
||||
render: (text, record) => <JobLineNotePopup jobline={record} />,
|
||||
render: (text, record) => (
|
||||
<JobLineNotePopup disabled={jobRO} jobline={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.location"),
|
||||
dataIndex: "location",
|
||||
key: "location",
|
||||
render: (text, record) => <JobLineLocationPopup jobline={record} />,
|
||||
render: (text, record) => (
|
||||
<JobLineLocationPopup jobline={record} disabled={jobRO} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.status"),
|
||||
@@ -244,9 +257,10 @@ export function JobLinesComponent({
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setJobLineEditContext({
|
||||
actions: { refetch: refetch },
|
||||
actions: { refetch: refetch, submit: form && form.submit },
|
||||
context: record,
|
||||
});
|
||||
}}
|
||||
@@ -308,12 +322,16 @@ export function JobLinesComponent({
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedLines.length > 0 ? false : true}
|
||||
disabled={
|
||||
!job.converted ||
|
||||
(selectedLines.length > 0 ? false : true) ||
|
||||
jobRO
|
||||
}
|
||||
onClick={() => {
|
||||
setPartsOrderContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: jobId,
|
||||
jobId: job.id,
|
||||
linesToOrder: selectedLines,
|
||||
},
|
||||
});
|
||||
@@ -343,10 +361,11 @@ export function JobLinesComponent({
|
||||
// />
|
||||
}
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setJobLineEditContext({
|
||||
actions: { refetch: refetch },
|
||||
context: { jobid: jobId },
|
||||
context: { jobid: job.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -370,7 +389,7 @@ export function JobLinesComponent({
|
||||
{record.parts_order_lines.map((item) => (
|
||||
<div key={item.id}>
|
||||
<Link
|
||||
to={`/manage/jobs/${jobId}?tab=partssublet&partsorderid=${item.parts_order.id}`}
|
||||
to={`/manage/jobs/${job.id}?tab=partssublet&partsorderid=${item.parts_order.id}`}
|
||||
>
|
||||
{item.parts_order.order_number || ""}
|
||||
</Link>
|
||||
@@ -396,4 +415,4 @@ export function JobLinesComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(JobLinesComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesComponent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import JobLinesComponent from "./job-lines.component";
|
||||
|
||||
function JobLinesContainer({ jobId, joblines, refetch }) {
|
||||
function JobLinesContainer({ job, joblines, refetch, form }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [selectedLines, setSelectedLines] = useState([]);
|
||||
|
||||
@@ -38,7 +38,8 @@ function JobLinesContainer({ jobId, joblines, refetch }) {
|
||||
setSearchText={setSearchText}
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
jobId={jobId}
|
||||
job={job}
|
||||
form={form}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import { Select, Button, Popover } from "antd";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -15,6 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function JobEmployeeAssignments({
|
||||
bodyshop,
|
||||
jobRO,
|
||||
body,
|
||||
refinish,
|
||||
prep,
|
||||
@@ -52,7 +55,7 @@ export function JobEmployeeAssignments({
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!assignment.employeeid}
|
||||
disabled={!assignment.employeeid || jobRO}
|
||||
onClick={() => {
|
||||
handleAdd(assignment);
|
||||
setVisibility(false);
|
||||
@@ -79,14 +82,18 @@ export function JobEmployeeAssignments({
|
||||
}`}</span>
|
||||
<MinusOutlined
|
||||
operation="body"
|
||||
onClick={() => handleRemove("body")}
|
||||
disabled={jobRO}
|
||||
onClick={() => !jobRO && handleRemove("body")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PlusCircleFilled
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setAssignment({ operation: "body" });
|
||||
setVisibility(true);
|
||||
if (!jobRO) {
|
||||
setAssignment({ operation: "body" });
|
||||
setVisibility(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -101,15 +108,19 @@ export function JobEmployeeAssignments({
|
||||
prep.last_name || ""
|
||||
}`}</span>
|
||||
<MinusOutlined
|
||||
disabled={jobRO}
|
||||
operation="prep"
|
||||
onClick={() => handleRemove("prep")}
|
||||
onClick={() => !jobRO && handleRemove("prep")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PlusCircleFilled
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setAssignment({ operation: "prep" });
|
||||
setVisibility(true);
|
||||
if (!jobRO) {
|
||||
setAssignment({ operation: "prep" });
|
||||
setVisibility(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -124,15 +135,19 @@ export function JobEmployeeAssignments({
|
||||
refinish.last_name || ""
|
||||
}`}</span>
|
||||
<MinusOutlined
|
||||
disabled={jobRO}
|
||||
operation="refinish"
|
||||
onClick={() => handleRemove("refinish")}
|
||||
onClick={() => !jobRO && handleRemove("refinish")}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<PlusCircleFilled
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setAssignment({ operation: "refinish" });
|
||||
setVisibility(true);
|
||||
if (!jobRO) {
|
||||
setAssignment({ operation: "refinish" });
|
||||
setVisibility(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { Button, Form, notification, Switch } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation, useParams } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { MARK_LATEST_APPOINTMENT_AS_ARRIVED } from "../../../../graphql/appointments.queries";
|
||||
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../../../redux/user/user.selectors";
|
||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||
import { logImEXEvent } from "../../../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobIntakeForm({ formItems, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [intakeJob] = useMutation(UPDATE_JOB);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
|
||||
const { jobId } = useParams();
|
||||
const history = useHistory();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
logImEXEvent("job_complete_intake");
|
||||
|
||||
const result = await intakeJob({
|
||||
variables: {
|
||||
jobId: jobId,
|
||||
job: {
|
||||
inproduction: values.addToProduction || false,
|
||||
status: bodyshop.md_ro_statuses.default_arrived || "Arrived*",
|
||||
actual_in: new Date(),
|
||||
scheduled_completion: values.scheduledCompletion,
|
||||
intakechecklist: values,
|
||||
scheduled_delivery: values.scheduledDelivery,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!!search.appointmentId) {
|
||||
const appUpdate = await markAptArrived({
|
||||
variables: { appointmentId: search.appointmentId },
|
||||
});
|
||||
|
||||
if (!!appUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("intake.errors.intake", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("intake.successes.intake") });
|
||||
history.push(`/manage/jobs/${jobId}`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("intake.errors.intake", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={{ addToProduction: true }}
|
||||
>
|
||||
{t("intake.labels.checklist")}
|
||||
|
||||
<ConfigFormComponents componentList={formItems} />
|
||||
|
||||
<Form.Item
|
||||
name="addToProduction"
|
||||
valuePropName="checked"
|
||||
label={t("intake.labels.addtoproduction")}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduledCompletion"
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduledDelivery"
|
||||
label={t("jobs.fields.scheduled_delivery")}
|
||||
>
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Button loading={loading} htmlType="submit">
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobIntakeForm);
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
export default function JobIntakeTemplateItem({ templateKey, renderTemplate }) {
|
||||
return (
|
||||
<div>
|
||||
{templateKey}
|
||||
<PrinterFilled onClick={() => renderTemplate(templateKey)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
import JobIntakeTemplateList from "./components/job-intake-template-list/job-intake-template-list.component";
|
||||
import JobIntakeForm from "./components/job-intake-form/job-intake-form.component";
|
||||
import { Row, Col } from "antd";
|
||||
export default function JobIntakeComponent({ intakeChecklistConfig }) {
|
||||
const { form, templates } = intakeChecklistConfig;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<JobIntakeTemplateList templates={templates} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<JobIntakeForm formItems={form} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobLineLocationPopup({ bodyshop, jobline }) {
|
||||
export function JobLineLocationPopup({ bodyshop, jobline, disabled }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [location, setLocation] = useState(jobline.location);
|
||||
@@ -73,7 +73,7 @@ export function JobLineLocationPopup({ bodyshop, jobline }) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
|
||||
onClick={() => setEditing(true)}
|
||||
onClick={() => !disabled && setEditing(true)}
|
||||
>
|
||||
{jobline.location}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMutation } from "react-apollo";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobLineNotePopup({ jobline }) {
|
||||
export default function JobLineNotePopup({ jobline, disabled }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [note, setNote] = useState(jobline.note);
|
||||
@@ -57,7 +57,7 @@ export default function JobLineNotePopup({ jobline }) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
|
||||
onClick={() => setEditing(true)}
|
||||
onClick={() => !disabled && setEditing(true)}
|
||||
>
|
||||
{jobline.notes}
|
||||
</div>
|
||||
|
||||
@@ -19,25 +19,26 @@ export default function JobLinesUpsertModalComponent({
|
||||
}, [visible, form]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
initialValues={jobLine}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
<Modal
|
||||
title={
|
||||
jobLine && jobLine.id
|
||||
? t("joblines.labels.edit")
|
||||
: t("joblines.labels.new")
|
||||
}
|
||||
forceRender
|
||||
visible={visible}
|
||||
width="60%"
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Modal
|
||||
title={
|
||||
jobLine && jobLine.id
|
||||
? t("joblines.labels.edit")
|
||||
: t("joblines.labels.new")
|
||||
}
|
||||
visible={visible}
|
||||
width="60%"
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
onCancel={handleCancel}
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
initialValues={jobLine}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
@@ -58,7 +59,7 @@ export default function JobLinesUpsertModalComponent({
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
||||
<Select>
|
||||
<Select allowClear>
|
||||
<Select.Option value="PAA">
|
||||
{t("joblines.fields.part_types.PAA")}
|
||||
</Select.Option>
|
||||
@@ -68,13 +69,22 @@ export default function JobLinesUpsertModalComponent({
|
||||
<Select.Option value="PAL">
|
||||
{t("joblines.fields.part_types.PAL")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAM">
|
||||
{t("joblines.fields.part_types.PAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAS">
|
||||
{t("joblines.fields.part_types.PAS")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAR">
|
||||
{t("joblines.fields.part_types.PAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="PAC">
|
||||
{t("joblines.fields.part_types.PAC")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||
<Select>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">
|
||||
{t("joblines.fields.lbr_types.LAA")}
|
||||
</Select.Option>
|
||||
@@ -84,6 +94,9 @@ export default function JobLinesUpsertModalComponent({
|
||||
<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>
|
||||
@@ -99,6 +112,21 @@ export default function JobLinesUpsertModalComponent({
|
||||
<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>
|
||||
</LayoutFormRow>
|
||||
@@ -109,12 +137,50 @@ export default function JobLinesUpsertModalComponent({
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("joblines.fields.mod_lb_hrs")} name="mod_lb_hrs">
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lb_hrs")}
|
||||
name="mod_lb_hrs"
|
||||
rules={[
|
||||
// {
|
||||
// required: !!form.getFieldValue("mod_lbr_ty"),
|
||||
// message: t("general.validation.required"),
|
||||
// },
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!!getFieldValue("mod_lbr_ty") === !!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("joblines.validation.hrsrequirediflbrtyp")
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputCurrency />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.part_qty")} name="part_qty">
|
||||
<Form.Item
|
||||
label={t("joblines.fields.part_qty")}
|
||||
name="part_qty"
|
||||
rules={[
|
||||
// {
|
||||
// required: !!form.getFieldValue("mod_lbr_ty"),
|
||||
// message: t("general.validation.required"),
|
||||
// },
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!!getFieldValue("part_type") === !!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("joblines.validation.requiredifparttype")
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("joblines.fields.db_price")} name="db_price">
|
||||
@@ -134,12 +200,22 @@ export default function JobLinesUpsertModalComponent({
|
||||
);
|
||||
},
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!!getFieldValue("part_type") === !!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("joblines.validation.requiredifparttype")
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputCurrency precision={2} min={0} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Modal>
|
||||
</Form>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "@apollo/react-hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -27,6 +27,7 @@ function JobLinesUpsertModalContainer({
|
||||
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
|
||||
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = (values) => {
|
||||
setLoading(true);
|
||||
if (!jobLineEditModal.context.id) {
|
||||
@@ -38,6 +39,7 @@ function JobLinesUpsertModalContainer({
|
||||
.then((r) => {
|
||||
if (jobLineEditModal.actions.refetch)
|
||||
jobLineEditModal.actions.refetch();
|
||||
//Need to recalcuate totals.
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("joblines.successes.created"),
|
||||
@@ -69,7 +71,12 @@ function JobLinesUpsertModalContainer({
|
||||
}),
|
||||
});
|
||||
});
|
||||
if (jobLineEditModal.actions.refetch) jobLineEditModal.actions.refetch();
|
||||
if (jobLineEditModal.actions.submit) {
|
||||
jobLineEditModal.actions.submit();
|
||||
} else {
|
||||
if (jobLineEditModal.actions.refetch)
|
||||
jobLineEditModal.actions.refetch();
|
||||
}
|
||||
toggleModalVisible();
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Checkbox, Statistic, Table } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import { Checkbox, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function JobReconciliationInvoiceTable({
|
||||
invoiceLineState,
|
||||
export default function JobReconciliationBillsTable({
|
||||
billLineState,
|
||||
invoiceLineData,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -15,12 +14,11 @@ export default function JobReconciliationInvoiceTable({
|
||||
sortedInfo: {},
|
||||
});
|
||||
|
||||
const [selectedLines, setSelectedLines] = invoiceLineState;
|
||||
const [total, setTotal] = useState(Dinero({ amount: 0 }).toFormat());
|
||||
const [selectedLines, setSelectedLines] = billLineState;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("invoicelines.fields.line_desc"),
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
@@ -28,7 +26,16 @@ export default function JobReconciliationInvoiceTable({
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.retail"),
|
||||
title: t("billlines.labels.from"),
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
render: (text, record) =>
|
||||
`${record.bill.vendor && record.bill.vendor.name} / ${
|
||||
record.bill.invoice_number
|
||||
}`,
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.retail"),
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
||||
@@ -39,7 +46,7 @@ export default function JobReconciliationInvoiceTable({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.actual_cost"),
|
||||
title: t("billlines.fields.actual_cost"),
|
||||
dataIndex: "actual_cost",
|
||||
key: "actual_cost",
|
||||
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
||||
@@ -50,7 +57,7 @@ export default function JobReconciliationInvoiceTable({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("invoicelines.fields.quantity"),
|
||||
title: t("billlines.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
@@ -58,14 +65,16 @@ export default function JobReconciliationInvoiceTable({
|
||||
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("invoices.fields.is_credit_memo"),
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sorter: (a, b) => a.bill.is_credit_memo - b.bill.is_credit_memo,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.is_credit_memo} />,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.bill.is_credit_memo} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,32 +83,16 @@ export default function JobReconciliationInvoiceTable({
|
||||
};
|
||||
const handleOnRowClick = (selectedRecordKeys, selectedRecords) => {
|
||||
setSelectedLines(selectedRecordKeys);
|
||||
calculateTotal(selectedRecords);
|
||||
};
|
||||
|
||||
const calculateTotal = (selectedRecords) => {
|
||||
let total = Dinero({ amount: 0 });
|
||||
selectedRecords.forEach(
|
||||
(record) =>
|
||||
(total = total.add(
|
||||
Dinero({
|
||||
amount:
|
||||
record.actual_price * 100 * (record.is_credit_memo ? -1 : 1),
|
||||
}).multiply(record.quantity)
|
||||
))
|
||||
);
|
||||
|
||||
setTotal(total.toFormat());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
size='small'
|
||||
title={() => <div></div>}
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: "40vh", x: true }}
|
||||
columns={columns}
|
||||
rowKey='id'
|
||||
rowKey="id"
|
||||
dataSource={invoiceLineData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
@@ -107,7 +100,6 @@ export default function JobReconciliationInvoiceTable({
|
||||
selectedRowKeys: selectedLines,
|
||||
}}
|
||||
/>
|
||||
<Statistic value={total} title='total' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,29 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import JobReconciliationInvoicesTable from "../job-reconciliation-invoices-table/job-reconciliation-invoices-table.component";
|
||||
import JobReconciliationBillsTable from "../job-reconciliation-bills-table/job-reconciliation-bills-table.component";
|
||||
import JobReconciliationPartsTable from "../job-reconciliation-parts-table/job-reconciliation-parts-table.component";
|
||||
import JobReconciliationTotals from "../job-reconciliation-totals/job-reconciliation-totals.component";
|
||||
|
||||
export default function JobReconciliationModalComponent({ job, invoices }) {
|
||||
export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
const jobLineState = useState([]);
|
||||
const invoiceLineState = useState([]);
|
||||
const billLineState = useState([]);
|
||||
|
||||
const invoiceLineData =
|
||||
invoices
|
||||
bills
|
||||
.map((i) =>
|
||||
i.invoicelines.map((il) => {
|
||||
return { ...il, is_credit_memo: i.is_credit_memo };
|
||||
i.billlines.map((il) => {
|
||||
return { ...il, bill: i };
|
||||
})
|
||||
)
|
||||
.flat() || [];
|
||||
|
||||
const jobLineData = job.joblines.filter((j) => j.part_type !== null);
|
||||
const jobLineData = job.joblines.filter(
|
||||
(j) => j.part_type !== null && j.part_type !== "PAE"
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<JobReconciliationPartsTable
|
||||
jobLineData={jobLineData}
|
||||
@@ -28,12 +31,20 @@ export default function JobReconciliationModalComponent({ job, invoices }) {
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<JobReconciliationInvoicesTable
|
||||
<JobReconciliationBillsTable
|
||||
invoiceLineData={invoiceLineData}
|
||||
invoiceLineState={invoiceLineState}
|
||||
billLineState={billLineState}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<JobReconciliationTotals
|
||||
jobLines={jobLineData}
|
||||
jobLineState={jobLineState}
|
||||
billLines={invoiceLineData}
|
||||
billLineState={billLineState}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,27 +5,22 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectReconciliation } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import JobReconciliationModalComponent from "./job-reconciliation-modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reconciliationModal: selectReconciliation,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("reconciliation")),
|
||||
});
|
||||
|
||||
function InvoiceEnterModalContainer({
|
||||
function JobReconciliationModalContainer({
|
||||
reconciliationModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { context, visible } = reconciliationModal;
|
||||
const { job, invoices } = context;
|
||||
const { job, bills } = context;
|
||||
|
||||
const handleCancel = () => {
|
||||
toggleModalVisible();
|
||||
@@ -36,11 +31,13 @@ function InvoiceEnterModalContainer({
|
||||
title={t("jobs.labels.reconciliationheader")}
|
||||
width={"90%"}
|
||||
visible={visible}
|
||||
okText={t("general.actions.save")}
|
||||
okText={t("general.actions.close")}
|
||||
onOk={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
destroyOnClose>
|
||||
<JobReconciliationModalComponent job={job} invoices={invoices} />
|
||||
cancelButtonProps={{ display: "none" }}
|
||||
destroyOnClose
|
||||
>
|
||||
<JobReconciliationModalComponent job={job} bills={bills} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -48,4 +45,4 @@ function InvoiceEnterModalContainer({
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InvoiceEnterModalContainer);
|
||||
)(JobReconciliationModalContainer);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Statistic, Table } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
@@ -16,20 +15,8 @@ export default function JobReconcilitionPartsTable({
|
||||
});
|
||||
|
||||
const [selectedLines, setSelectedLines] = jobLineState;
|
||||
const [total, setTotal] = useState(Dinero({ amount: 0 }).toFormat());
|
||||
|
||||
const columns = [
|
||||
// {
|
||||
// title: t("joblines.fields.line_no"),
|
||||
// dataIndex: "line_no",
|
||||
// key: "line_no",
|
||||
// sorter: (a, b) => a.line_no - b.line_no,
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
|
||||
// //ellipsis: true,
|
||||
// editable: true,
|
||||
// width: 75,
|
||||
// },
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
@@ -38,32 +25,32 @@ export default function JobReconcilitionPartsTable({
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.oem_partno ? a.oem_partno : a.op_code_desc,
|
||||
b.oem_partno ? b.oem_partno : b.op_code_desc
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
||||
// {
|
||||
// title: t("joblines.fields.oem_partno"),
|
||||
// dataIndex: "oem_partno",
|
||||
// key: "oem_partno",
|
||||
// sorter: (a, b) =>
|
||||
// alphaSort(
|
||||
// a.oem_partno ? a.oem_partno : a.op_code_desc,
|
||||
// b.oem_partno ? b.oem_partno : b.op_code_desc
|
||||
// ),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.oem_partno ? record.oem_partno : record.op_code_desc}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
|
||||
},
|
||||
// render: (text, record) => (
|
||||
// <span>
|
||||
// {record.oem_partno ? record.oem_partno : record.op_code_desc}
|
||||
// </span>
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// title: t("joblines.fields.part_type"),
|
||||
// dataIndex: "part_type",
|
||||
// key: "part_type",
|
||||
// sorter: (a, b) => alphaSort(a.part_type, b.part_type),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
|
||||
// },
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
@@ -95,14 +82,7 @@ export default function JobReconcilitionPartsTable({
|
||||
</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.mod_lb_hrs"),
|
||||
dataIndex: "mod_lb_hrs",
|
||||
key: "mod_lb_hrs",
|
||||
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.status"),
|
||||
dataIndex: "status",
|
||||
@@ -118,32 +98,16 @@ export default function JobReconcilitionPartsTable({
|
||||
};
|
||||
const handleOnRowClick = (selectedRecordKeys, selectedRecords) => {
|
||||
setSelectedLines(selectedRecordKeys);
|
||||
calculateTotal(selectedRecords);
|
||||
};
|
||||
|
||||
const calculateTotal = (selectedRecords) => {
|
||||
let total = Dinero({ amount: 0 });
|
||||
selectedRecords.forEach(
|
||||
(record) =>
|
||||
(total = total.add(
|
||||
Dinero({ amount: record.act_price * 100 }).multiply(record.part_qty)
|
||||
))
|
||||
);
|
||||
|
||||
setTotal(total.toFormat());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
size='small'
|
||||
title={() => (
|
||||
<div>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
rowKey='id'
|
||||
scroll={{ y: "40vh", x: true }}
|
||||
rowKey="id"
|
||||
dataSource={jobLineData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
@@ -151,7 +115,6 @@ export default function JobReconcilitionPartsTable({
|
||||
selectedRowKeys: selectedLines,
|
||||
}}
|
||||
/>
|
||||
<Statistic value={total} title='total' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||