Merge branch 'dev-patrick' into development

This commit is contained in:
Patrick Fic
2020-10-06 09:42:25 -07:00
503 changed files with 85486 additions and 7858 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

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

View File

@@ -68,5 +68,5 @@
.ant-table-cell {
// background-color: red;
padding: 0.2rem !important;
//padding: 0.2rem !important;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => {
value={value ? moment(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur}
{...restProps}
/>
</div>

View File

@@ -6,7 +6,7 @@ import { TimePicker } from "antd";
import moment from "moment";
//To be used as a form element only.
const DateTimePicker = ({ value, onChange, onBlur }, 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,6 @@
max-height: 3.5rem;
}
.header-main-menu {
width: 80vw;
float: left;
//width: 95vw;
//float: left;
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
.job-invoices-totals-container {
.job-bills-totals-container {
margin: 0rem 2rem;
display: flex;
flex-direction: column;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
) : (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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