Compare commits

..

7 Commits

Author SHA1 Message Date
Patrick Fic
26dc720929 Add technician side to part dispatching. 2023-08-04 13:24:41 -07:00
Patrick Fic
b5cc1c06df Merge branch 'release/2023-08-11' into feature/IO-2278-parts-dispatching 2023-08-04 12:36:30 -07:00
Patrick Fic
786c790307 WIP Tech Claiming Ability 2023-08-04 12:28:08 -07:00
Patrick Fic
de102d9898 IO-2278 Parts Dispatching tables & buttons. 2023-06-07 08:25:49 -07:00
Patrick Fic
4dc3bc1532 Merge branch 'release/2023-06-09' into feature/IO-2278-parts-dispatching 2023-06-06 13:33:25 -07:00
Patrick Fic
50da8cfbc6 Merge branch 'release/2023-06-09' into feature/IO-2278-parts-dispatching 2023-06-06 13:20:10 -07:00
Patrick Fic
68f4237e15 IO-2278 Begin parts dispatching including basic items and schema. 2023-06-01 13:27:08 -07:00
167 changed files with 15805 additions and 35694 deletions

View File

@@ -8,13 +8,13 @@ orbs:
jobs:
api-deploy:
docker:
- image: cimg/node:18.18.2
- image: "cimg/base:stable"
steps:
- checkout
- eb/setup
- run:
command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init imex-online-production-api -r ca-central-1 -p "Node.js 16 running on 64bit Amazon Linux 2"
eb status --verbose
eb deploy
eb status

4
.gitignore vendored
View File

@@ -117,6 +117,4 @@ logs/oAuthClient-log.log
.node-persist/**
/*.env.*
.idea/*
.idea
/*.env.*

1
.npmrc
View File

@@ -1 +0,0 @@
legacy-peer-deps=true

View File

@@ -364,27 +364,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>unblock</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>viewjob</name>
<definition_loaded>false</definition_loaded>
@@ -1030,6 +1009,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>smspaymentreminder</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>suggesteddates</name>
<definition_loaded>false</definition_loaded>
@@ -16228,27 +16228,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>copied</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>copylink</name>
<definition_loaded>false</definition_loaded>
@@ -16585,27 +16564,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sendbysms</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>senderrortosupport</name>
<definition_loaded>false</definition_loaded>
@@ -17776,27 +17734,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>reports</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>required</name>
<definition_loaded>false</definition_loaded>
@@ -19339,27 +19276,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>openingip</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<definition_loaded>false</definition_loaded>
@@ -19514,27 +19430,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>paymentnum</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>paymenttype</name>
<definition_loaded>false</definition_loaded>
@@ -19629,6 +19524,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dispatchparts</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>new</name>
<definition_loaded>false</definition_loaded>
@@ -24138,27 +24054,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dms_model_override</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>dms_unsold</name>
<definition_loaded>false</definition_loaded>
@@ -33110,6 +33005,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>paymentremindersms</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>phonebook</name>
<definition_loaded>false</definition_loaded>
@@ -34080,6 +33996,27 @@
<folder_node>
<name>tech</name>
<children>
<concept_node>
<name>dispatchedparts</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>home</name>
<definition_loaded>false</definition_loaded>
@@ -36168,6 +36105,188 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_dispatch</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>accept</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>accepting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>creating</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>number</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>percent_accepted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>parts_dispatch</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_dispatch_lines</name>
<children>
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>accepted_at</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>parts_orders</name>
<children>
@@ -37314,32 +37433,6 @@
<folder_node>
<name>payments</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>generatepaymentlink</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
@@ -37385,27 +37478,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inserting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -37878,27 +37950,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>smspaymentreminder</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<definition_loaded>false</definition_loaded>
@@ -43344,27 +43395,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_closed_estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_closed_ins_co</name>
<definition_loaded>false</definition_loaded>
@@ -43449,27 +43479,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_open_estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_open_ins_co</name>
<definition_loaded>false</definition_loaded>
@@ -43554,27 +43563,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_closed_estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_closed_ins_co</name>
<definition_loaded>false</definition_loaded>
@@ -43659,27 +43647,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_open_estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_open_ins_co</name>
<definition_loaded>false</definition_loaded>
@@ -44016,27 +43983,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>open_orders_referral</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>open_orders_specific_csr</name>
<definition_loaded>false</definition_loaded>
@@ -46416,27 +46362,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>created_by</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>date</name>
<definition_loaded>false</definition_loaded>

24522
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"@apollo/client": "^3.7.9",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
@@ -89,8 +89,8 @@
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",

View File

@@ -0,0 +1,49 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
function Test({ bodyshop, setEmailOptions }) {
return (
<div>
<button
onClick={() => {
setEmailOptions({
messageOptions: {
to: ["patrickwf@gmail.com"],
replyTo: bodyshop.email,
},
template: {
name: TemplateList().parts_order.key,
variables: {
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
},
},
});
}}
>
send email
</button>
<button
onClick={() => {
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
}}
>
Log an ImEX Event.
</button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -1,63 +0,0 @@
{
"status": 24201299,
"custid": 19607899,
"paymentid": 24201299,
"response": "A",
"authcode": "498680",
"declinereason": "Approved",
"fee": 0,
"invoice": "",
"account": "john",
"amount": 1000,
"amountincludesfee": false,
"total": 1000,
"paymenttype": "C",
"methodhint": "VI ***1111",
"cardbrand": "Visa",
"cardnumdisplay": "***1111",
"receiptelements": {
"authcode": "498680",
"cust_srv_ph_num": "1-555-555-5555",
"rcpt_pg_ftr_txt": "Thank You\nPlease Come Again",
"rcpt_currency": "USD",
"responsecode": "A",
"rcpt_pay_mthd": "Visa",
"transid": "C00 915799",
"merch_disp_nm": "CP Devel Test",
"rcpt_input_mthd": "Keyed",
"rcpt_pg_hdr_txt": "Welcome!",
"rcpt_tran_time": "Thursday February 23 2023, 11:25:36 pm +08",
"rcpt_trans_type": "Normal Transaction (Sale)",
"message": "Approved",
"rcpt_dba_addr": "1234 Storefront Ave\nSome City, UT 84111",
"avsdata": "N",
"receiptrequirements": "S",
"rcpt_cardnum": "************1111",
"cv2result": "M",
"rfnd_policy_txt": "<b>No Refunds</b>\nStore Credit Only",
"labels": {
"tranref": "REF#",
"tid": "TID",
"validationcode": "ValCode",
"emvapplicationid": "AID",
"emvatc": "ATC",
"rcpt_pay_mthd": "Pay Method",
"transid": "TransID",
"rcpt_input_mthd": "IMode",
"emvtsi": "TSI",
"emvac": "AC",
"rcpt_trans_type": "TranType",
"emvapplicationname": "PApp",
"visarewards": "RewardsProg"
}
},
"receipttoken": "H4sIAAAAAAAAACXMTQuCMBgA4P/ynh3tw_3dBI/ipQ8NOtRN53QiblpBRfTfCzo/8LwhxGAdZCCwFYoJJFQjI2kvHdGu74lVkgmrWyWNhASW5jW7cB87yHjKKePGJODnxrrnMl7dDTKmEJlSOqV/_N30XPpyj2Eddq57_KKZ8FLzmh_G1VQnVfhjiXGK1XYTc/h8AVOkf4qUAAAA",
"call": "card_payment",
"nonce": "488b5568-b5c1-4f38-8b2f-3b050f3abb11P",
"hmac": "JyPAJ9Yx0SlYBTtqns1OxAFRt+xF3l2UiLPO5zTDRBE=",
"paymentreferenceid": "C19607899P24201299",
"cardnum": "...1111",
"email": "",
"nameOnCard": "John Allen",
"cardType": "visa"
}

View File

@@ -0,0 +1,9 @@
import React from "react";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
export default function Test() {
return (
<div>
<QboAuthorizeComponent />
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setRefundPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
});
function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal);
return (
<div>
<Button
onClick={() =>
setRefundPaymentContext({
context: {},
})
}
>
Open Modal
</Button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
const Templates = TemplateList("job_special");
@@ -143,7 +143,7 @@ function BillEnterModalContainer({
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
const adjKeys = Object.keys(adjustmentsToInsert);
@@ -316,9 +316,7 @@ function BillEnterModalContainer({
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
});
if (enterAgain) {

View File

@@ -50,7 +50,6 @@ export function BillFormComponent({
job,
loadOutstandingReturns,
loadInventory,
preferredMake,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -186,7 +185,6 @@ export function BillFormComponent({
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>

View File

@@ -1,16 +1,16 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -59,7 +59,6 @@ export function BillFormContainer({
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
/>
{!billEdit && (
<BillCmdReturnsTableComponent

View File

@@ -1,374 +0,0 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
Col,
Form,
Input,
Row,
Space,
Spin,
Statistic,
notification,
} from "antd";
import axios from "axios";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS,
} from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
const CardPaymentModalComponent = ({
bodyshop,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail,
}) => {
const { context } = cardPaymentModal;
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const [, { data, refetch, queryLoading }] = useLazyQuery(
QUERY_RO_AND_OWNER_BY_JOB_PKS,
{
variables: { jobids: [context.jobid] },
skip: true,
}
);
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
window.intellipay.runOnApproval(async function (response) {
console.warn("*** Running On Approval Script ***");
form.setFieldValue("paymentResponse", response);
form.submit();
});
window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response,
})),
},
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
);
});
};
const handleFinish = async (values) => {
try {
await insertPayment({
variables: {
paymentInput: values.payments.map((payment) => ({
amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand,
jobid: payment.jobid,
date: moment(Date.now()),
payment_responses: {
data: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
});
toggleModalVisible();
} catch (error) {
console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message }),
});
} finally {
setLoading(false);
}
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
});
if (window.intellipay) {
// eslint-disable-next-line no-eval
eval(response.data);
SetIntellipayCallbackFunctions();
window.intellipay.autoOpen();
} else {
var rg = document.createRange();
let node = rg.createContextualFragment(response.data);
document.documentElement.appendChild(node);
SetIntellipayCallbackFunctions();
window.intellipay.isAutoOpen = true;
window.intellipay.initialize();
}
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip"),
});
setLoading(false);
}
};
return (
<Card title="Card Payment">
<Spin spinning={loading}>
<Form
onFinish={handleFinish}
form={form}
layout="vertical"
initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [],
}}
>
<Form.List name={["payments"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelectComponent
notExported={false}
clm_no
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{ margin: "1rem" }}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
>
{() => {
console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue();
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) });
}
console.log(
"Acc info",
data,
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
);
return (
<>
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
}}
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.amount).join() !==
curValues.payments?.map((p) => p?.amount).join()
}
>
{() => {
const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
return (
<Space style={{ float: "right" }}>
<Statistic
title="Amount To Charge"
value={totalAmountToCharge}
precision={2}
/>
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
</Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form>
</Spin>
</Card>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalComponent);

View File

@@ -1,57 +0,0 @@
import { Button, Modal } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CardPaymentModalComponent from "./card-payment-modal.component.";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
function CardPaymentModalContainer({
cardPaymentModal,
toggleModalVisible,
bodyshop,
}) {
const { visible } = cardPaymentModal;
const { t } = useTranslation();
const handleCancel = () => {
toggleModalVisible();
};
const handleOK = () => {
toggleModalVisible();
};
return (
<Modal
open={visible}
onOk={handleOK}
onCancel={handleCancel}
footer={[
<Button key="back" onClick={handleCancel}>
{t("job_payments.buttons.goback")}
</Button>,
]}
width="80%"
destroyOnClose
>
<CardPaymentModalComponent />
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalContainer);

View File

@@ -8,23 +8,15 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
searchingForConversation: searchingForConversation,
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
});
export function ChatOpenButton({
bodyshop,
searchingForConversation,
phone,
jobid,
openChatByPhone,
}) {
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
const { t } = useTranslation();
if (!phone) return <></>;
@@ -37,7 +29,7 @@ export function ChatOpenButton({
onClick={(e) => {
e.stopPropagation();
const p = parsePhoneNumber(phone, "CA");
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
if (p && p.isValid()) {
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
} else {

View File

@@ -2,10 +2,10 @@ import { useQuery } from "@apollo/client";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -15,7 +15,6 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
isConverted: true,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",

View File

@@ -4,14 +4,14 @@ import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
import { setModalContext } from "../../redux/modals/modals.actions";
import moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import moment from "moment";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({

View File

@@ -34,18 +34,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{/* <FormFieldsChanged form={form} /> */}
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
<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.make")}
name="make"
@@ -70,6 +58,18 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
<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"
@@ -118,7 +118,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
},
]}
>
<InputNumber min={0} precision={0} />
<InputNumber precision={0} />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fleetnumber")}
@@ -213,24 +213,49 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
>
<CourtesyCarStatus />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber min={0} precision={0} />
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm
p.mileage !== c.mileage ||
p.nextservicedate !== c.nextservicedate ||
p.nextservicekm !== c.nextservicekm
}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver =
nextservicekm <= form.getFieldValue("mileage");
if (mileageOver)
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
@@ -238,36 +263,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{t("contracts.labels.cardueforservice")}
</span>
<span>{`${nextservicekm} km`}</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}
>
{() => {
const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService =
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
if (dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.cardueforservice")}
</span>
<span>
<DateFormatter>{nextservicedate}</DateFormatter>
</span>
@@ -288,6 +283,12 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
@@ -299,8 +300,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
{() => {
const expires = form.getFieldValue("registrationexpires");
const dateover =
expires && moment(expires).endOf("day").isBefore(moment());
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
@@ -330,13 +330,14 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}
shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
>
{() => {
const expires = form.getFieldValue("insuranceexpires");
const dateover =
expires && moment(expires).endOf("day").isBefore(moment());
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (

View File

@@ -9,15 +9,15 @@ import {
Table,
Tooltip,
} from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
@@ -115,14 +115,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
@@ -174,7 +166,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
(c.plate || "").toLowerCase().includes(searchText.toLowerCase()) ||
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
)
: courtesycars;

View File

@@ -23,40 +23,36 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
if (item.job) {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
});
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [
{
@@ -186,12 +182,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
};
return (
<Card
title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}

View File

@@ -18,7 +18,7 @@ export default function DataLabel({
<div {...props} style={{ display: "flex" }}>
<div
style={{
// flex: 2,
flex: 2,
marginRight: ".2rem",
}}
>

View File

@@ -191,13 +191,6 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
>
<Switch />
</Form.Item>
<Form.Item
name="dms_model_override"
label={t("jobs.fields.dms.dms_model_override")}
initialValue={false}
>
<Switch />
</Form.Item>
</Space>
</div>
)}

View File

@@ -109,7 +109,7 @@ export function EmailOverlayContainer({
]
: []),
],
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.fullsize),
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
//attachments,
});
notification["success"]({ message: t("emails.successes.sent") });

View File

@@ -54,7 +54,6 @@ export default function GlobalSearchOs() {
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span>
<span>{`${job.plate_no || ""}`}</span>
</Space>
</Link>
),

View File

@@ -1,9 +1,10 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, {
BankFilled,
BarChartOutlined,
CarFilled,
CheckCircleOutlined,
ClockCircleFilled,
CheckCircleOutlined,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
@@ -25,7 +26,6 @@ import Icon, {
UnorderedListOutlined,
UserOutlined,
} from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -70,8 +70,6 @@ const mapDispatchToProps = (dispatch) => ({
setReportCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
signOutStart: () => dispatch(signOutStart()),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
});
function Header({
@@ -85,7 +83,6 @@ function Header({
setPaymentContext,
setReportCenterContext,
recentItems,
setCardPaymentContext,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
@@ -97,11 +94,6 @@ function Header({
{},
bodyshop && bodyshop.imexshopid
);
const { ImEXPay } = useTreatments(
["ImEXPay"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
@@ -248,20 +240,6 @@ function Header({
>
{t("menus.header.enterpayment")}
</Menu.Item>
{ImEXPay.treatment === "on" && (
<Menu.Item
key="entercardpayments"
onClick={() => {
setCardPaymentContext({
actions: {},
context: {},
});
}}
icon={<Icon component={FaCreditCard} />}
>
{t("menus.header.entercardpayment")}
</Menu.Item>
)}
<Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">
@@ -274,11 +252,7 @@ function Header({
onClick={() => {
setTimeTicketContext({
actions: {},
context: {
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
context: {},
});
}}
>

View File

@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -14,14 +13,13 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
const [isModalVisible, setIsModalVisible] = useState(false);
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -214,9 +212,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
]}
>
<Radio.Group>
{!technician ? (
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
) : null}
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
</Radio.Group>
</Form.Item>

View File

@@ -4,7 +4,6 @@ import {
Divider,
Dropdown,
Form,
Input,
Menu,
notification,
Popover,
@@ -30,13 +29,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
import { useMutation } from "@apollo/client";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -61,44 +58,16 @@ export function ScheduleEventComponent({
const [visible, setVisible] = useState(false);
const history = useHistory();
const searchParams = queryString.parse(useLocation().search);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const [title, setTitle] = useState(event.title);
const blockContent = (
<Space direction="vertical" wrap>
<Input
value={title}
onChange={(e) => setTitle(e.currentTarget.value)}
onBlur={async () => {
await updateAppointment({
variables: {
appid: event.id,
app: {
title: title,
},
},
optimisticResponse: {
update_appointments: {
__typename: "appointments_mutation_response",
returning: [
{
...event,
title: title,
__typename: "appointments",
},
],
},
},
});
}}
/>
const blockContent = (
<div>
<Button
onClick={() => handleCancel({ id: event.id })}
disabled={event.arrived}
>
{t("appointments.actions.unblock")}
{t("appointments.actions.cancel")}
</Button>
</Space>
</div>
);
const popoverContent = (
@@ -239,56 +208,46 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
{event.arrived ? (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
) : (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
)}
</Popover>
{event.isintake ? (
<Button
disabled={event.arrived}

View File

@@ -2,16 +2,12 @@ import { useMutation } from "@apollo/client";
import { notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENT_BY_ID } from "../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import ScheduleEventComponent from "./schedule-event.component";
export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const dispatch = useDispatch();
const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB);
@@ -38,24 +34,16 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported,
},
},
});
if (!jobUpdate.errors) {
dispatch(
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
})
);
}
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.updating", {

View File

@@ -1,6 +1,5 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, notification, Switch } from "antd";
import moment from "moment-business-days";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -13,15 +12,16 @@ import {
MARK_LATEST_APPOINTMENT_ARRIVED,
} from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
import moment from "moment-business-days";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -230,7 +230,6 @@ export function JobChecklistForm({
)),
scheduled_delivery:
job.scheduled_delivery && moment(job.scheduled_delivery),
production_vars: job.production_vars,
}),
...(type === "deliver" && {
removeFromProduction: true,

View File

@@ -8,7 +8,18 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
export default function JobLinesExpander({ jobline, jobid }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only",
@@ -23,7 +34,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
return (
<Row>
<Col md={24} lg={12}>
<Col md={24} lg={8}>
<Typography.Title level={4}>
{t("parts_orders.labels.parts_orders")}
</Typography.Title>
@@ -49,7 +60,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
)}
</Timeline>
</Col>
<Col md={24} lg={12}>
<Col md={24} lg={8}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Timeline>
{data.billlines.length > 0 ? (
@@ -71,7 +82,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
@@ -89,6 +100,37 @@ export default function JobLinesExpander({ jobline, jobid }) {
)}
</Timeline>
</Col>
<Col md={24} lg={8}>
<Typography.Title level={4}>
{t("parts_dispatch.labels.parts_dispatch")}
</Typography.Title>
<Timeline>
{data.parts_dispatch_lines.length > 0 ? (
data.parts_dispatch_lines.map((line) => (
<Timeline.Item key={line.id}>
<Space split={<Divider type="vertical" />} wrap>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>
{line.parts_dispatch.number}
</Link>
{
bodyshop.employees.find(
(e) => e.id === line.parts_dispatch.employeeid
)?.first_name
}
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
</Space>
</Timeline.Item>
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}
</Timeline>
</Col>
</Row>
);
}

View File

@@ -45,6 +45,7 @@ import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -106,7 +107,9 @@ export function JobLinesComponent({
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
...(record.critical || true
? { boxShadow: " -.5em 0 0 #FFC107" }
: {}),
},
}),
sortOrder:
@@ -121,10 +124,21 @@ export function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.parts_dispatch_lines[0]?.accepted_at || true
? { boxShadow: " -.5em 0 0 #FFC107" }
: {}),
},
}),
render: (text, record) => (
<span class="ant-table-cell-content">
{`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim()}
</span>
),
},
{
title: t("joblines.fields.op_code_desc"),
@@ -401,7 +415,7 @@ export function JobLinesComponent({
const markedTypes = [e.key];
if (e.key === "PAN") markedTypes.push("PAP");
if (e.key === "PAS") markedTypes.push("PASL");
setSelectedLines((selectedLines) =>
setSelectedLines(
_.uniq([
...selectedLines,
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
@@ -440,6 +454,11 @@ export function JobLinesComponent({
</Space>
</Tag>
)}
<JobLineDispatchButton
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
job={job}
/>
<Button
disabled={
(job && !job.converted) ||
@@ -448,15 +467,6 @@ export function JobLinesComponent({
technician
}
onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({
actions: { refetch: refetch },
context: {
@@ -614,17 +624,8 @@ export function JobLinesComponent({
onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows);
},
onSelect: (record, selected, selectedRows, nativeEvent) => {
if (selected) {
setSelectedLines((selectedLines) =>
_.uniqBy([...selectedLines, record], "id")
);
} else {
setSelectedLines((selectedLines) =>
selectedLines.filter((l) => l.id !== record.id)
);
}
},
onSelect: (record, selected, selectedRows, nativeEvent) =>
setSelectedLines(selectedRows),
}}
/>
</div>

View File

@@ -0,0 +1,167 @@
import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { Button, Form, Popover, Select, Space, notification } from "antd";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_PARTS_DISPATCH } from "../../graphql/parts-dispatch.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLineDispatchButton);
export function JobLineDispatchButton({
setSelectedLines,
selectedLines,
bodyshop,
jobRO,
job,
currentUser,
}) {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const Templates = TemplateList("job_special", {
ro_number: job.ro_number,
});
const { t } = useTranslation();
const [dispatchLines] = useMutation(INSERT_PARTS_DISPATCH);
const handleConvert = async (values) => {
try {
setLoading(true);
//THIS HAS NOT YET BEEN TESTED. START BY FINISHING THIS FUNCTION.
const result = await dispatchLines({
variables: {
partsDispatch: {
dispatched_at: moment(),
employeeid: values.employeeid,
jobid: job.id,
dispatched_by: currentUser.email,
parts_dispatch_lines: {
data: selectedLines.map((l) => ({
joblineid: l.id,
quantity: l.part_qty,
})),
},
},
//joblineids: selectedLines.map((l) => l.id),
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: result.errors,
}),
});
} else {
setSelectedLines([]);
await GenerateDocument(
{
name: Templates.parts_dispatch.key,
variables: {
id: result.data.insert_parts_dispatch_one.id,
},
},
{},
"p"
);
}
setVisible(false);
} catch (error) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.creating", {
error: error,
}),
});
} finally {
setLoading(false);
}
};
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleConvert}>
<Form.Item
name={"employeeid"}
label={t("timetickets.fields.employee")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
showSearch
style={{ width: 200 }}
optionFilterProp="children"
filterOption={(input, option) =>
option.props.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>
<Button
type="danger"
onClick={() => form.submit()}
loading={loading}
disabled={selectedLines.length === 0}
>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
);
return (
<Popover open={visible} content={popMenu}>
<Button
disabled={selectedLines.length === 0 || jobRO}
loading={loading}
onClick={() => setVisible(true)}
>
{t("joblines.actions.dispatchparts", { count: selectedLines.length })}
</Button>
</Popover>
);
}

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { notification, Select } from "antd";
import { notification, Select, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -56,10 +56,8 @@ export function JobLineLocationPopup({ bodyshop, jobline, disabled }) {
<LoadingSpinner loading={loading}>
<Select
autoFocus
allowClear
dropdownMatchSelectWidth={100}
value={location}
onClear={() => setLocation(null)}
onSelect={handleChange}
onBlur={handleSave}
>
@@ -77,7 +75,10 @@ export function JobLineLocationPopup({ bodyshop, jobline, disabled }) {
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
onClick={() => !disabled && setEditing(true)}
>
{jobline.location}
<Space wrap>
{jobline.location}
{jobline.parts_dispatch_lines.length > 0 && "-Disp"}
</Space>
</div>
);
}

View File

@@ -8,7 +8,7 @@ export default function JobLinesBillRefernece({ jobline }) {
return (
<div style={{ color: subletRequired && "tomato" }}>
{subletRequired && <WarningFilled />}
{`${billLine.actual_price.toFixed(2)} x ${billLine.quantity} (${
{`${(billLine.actual_price * billLine.quantity).toFixed(2)} (${
billLine.bill.vendor.name
} #${billLine.bill.invoice_number})`}
</div>

View File

@@ -1,26 +1,19 @@
import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd";
import { EditFilled } from "@ant-design/icons";
import Dinero from "dinero.js";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import {
openChatByPhone,
setMessage,
} from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -30,34 +23,20 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
});
export function JobPayments({
job,
jobRO,
bodyshop,
setMessage,
openChatByPhone,
setPaymentContext,
setCardPaymentContext,
refetch,
}) {
const { ImEXPay } = useTreatments(
["ImEXPay"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const columns = [
{
title: t("payments.fields.date"),
@@ -170,21 +149,6 @@ export function JobPayments({
title={t("payments.labels.title")}
extra={
<Space wrap>
{ImEXPay.treatment === "on" && (
<>
<Button
onClick={() =>
setCardPaymentContext({
actions: { refetch },
context: { jobid: job.id, balance },
})
}
>
{t("menus.header.entercardpayment")}
</Button>
<PaymentsGenerateLink job={job} />
</>
)}
<Button
disabled={!job.converted}
onClick={() =>
@@ -196,7 +160,6 @@ export function JobPayments({
>
{t("menus.header.enterpayment")}
</Button>
<DataLabel
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
label={t("payments.labels.balance")}
@@ -215,11 +178,6 @@ export function JobPayments({
scroll={{
x: true,
}}
expandable={{
expandedRowRender: (record) => (
<PaymentExpandedRowComponent record={record} bodyshop={bodyshop} />
),
}}
summary={() => (
<>
<Table.Summary.Row>

View File

@@ -33,9 +33,7 @@ const JobSearchSelect = (
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
console.log(v);
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
if (v && v !== "") callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -13,7 +13,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -54,7 +53,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
operation: AuditTrailMapping.admin_jobfieldchange(
key,
changedAuditFields[key] instanceof moment
? DateTimeFormat(changedAuditFields[key])
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key]
),
});
@@ -180,12 +179,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_lost_sale")}
name="date_lost_sale"
>
<DateTimePicker />
</Form.Item>
</LayoutFormRow>
</Form>

View File

@@ -3,7 +3,7 @@ import {
useApolloClient,
useLazyQuery,
useMutation,
useQuery,
useQuery
} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd";
@@ -20,7 +20,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import {
DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_JOBS,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK
} from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
@@ -28,7 +28,7 @@ import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
selectCurrentUser
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
@@ -73,8 +73,6 @@ export function JobsAvailableContainer({
const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
const [insertLoading, setInsertLoading] = useState(false);
const [insertNote] = useMutation(INSERT_NEW_NOTE);
@@ -96,7 +94,6 @@ export function JobsAvailableContainer({
logImEXEvent("job_import_new");
setOwnerModalVisible(false);
setInsertLoading(true);
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
@@ -123,7 +120,7 @@ export function JobsAvailableContainer({
let existingVehicles;
if (estData.est_data.v_vin) {
//There's vehicle data, need to double-check the VIN.
//There's vehicle data, need to double check the VIN.
existingVehicles = await client.query({
query: SEARCH_VEHICLE_BY_VIN,
variables: {
@@ -138,7 +135,6 @@ export function JobsAvailableContainer({
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"),
job_totals: newTotals,
date_open: moment(),
status: bodyshop.md_ro_statuses.default_imported,
notes: {
data: {
created_by: currentUser.email,
@@ -146,7 +142,7 @@ export function JobsAvailableContainer({
text: t("jobs.labels.importnote"),
},
},
queued_for_parts: partsQueueToggle,
queued_for_parts: true,
...(existingVehicles && existingVehicles.data.vehicles.length > 0
? { vehicleid: existingVehicles.data.vehicles[0].id, vehicle: null }
: {}),
@@ -160,51 +156,46 @@ export function JobsAvailableContainer({
delete newJob.vehicle;
}
try {
const r = await insertNewJob({
variables: {
job: newJob,
},
});
insertNewJob({
variables: {
job: newJob,
},
})
.then((r) => {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id);
}
notification["success"]({
message: t("jobs.successes.created"),
onClick: () => {
history.push(`/manage/jobs/${r.data.insert_jobs.returning[0].id}`);
},
});
//Job has been inserted. Clean up the available jobs record.
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id);
}
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
});
notification["success"]({
message: t("jobs.successes.created"),
onClick: () => {
history.push(`/manage/jobs/${r.data.insert_jobs.returning[0].id}`);
},
});
//Job has been inserted. Clean up the available jobs record.
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
});
deleteJob({
variables: { id: estData.id },
}).then((r) => {
deleteJob({
variables: { id: estData.id },
}).then((r) => {
refetch();
setInsertLoading(false);
});
})
.catch((r) => {
//error while inserting
notification["error"]({
message: t("jobs.errors.creating", { error: r.message }),
});
refetch();
setInsertLoading(false);
});
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
} catch (err) {
//error while inserting
notification["error"]({
message: t("jobs.errors.creating", { error: err.message }),
});
refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)});
setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
}
};
//Supplement scenario
//Suplement scenario
const onJobFindModalOk = async () => {
logImEXEvent("job_import_supplement");
@@ -256,14 +247,11 @@ export function JobsAvailableContainer({
// "0.00"
// ),
// job_totals: newTotals,
queued_for_parts: partsQueueToggle,
// queued_for_parts: true,
},
},
});
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
if (CriticalPartsScanning.treatment === "on") {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
}
if (updateResult.errors) {
@@ -338,14 +326,12 @@ export function JobsAvailableContainer({
const onOwnerModalCancel = () => {
setOwnerModalVisible(false);
setSelectedOwner(null);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
};
const onJobModalCancel = () => {
setJobModalVisible(false);
modalSearchState[1]("");
setSelectedJob(null);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
};
const addJobAsNew = (record) => {
@@ -366,8 +352,6 @@ export function JobsAvailableContainer({
}, [addJobAsSupp, availableJobId, clm_no]);
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
@@ -377,14 +361,11 @@ export function JobsAvailableContainer({
loading={estDataRaw.loading}
error={estDataRaw.error}
owner={owner}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
visible={ownerModalVisible}
onOk={onOwnerFindModalOk}
onCancel={onOwnerModalCancel}
/>
<JobsFindModalContainer
loading={estDataRaw.loading}
@@ -396,8 +377,6 @@ export function JobsAvailableContainer({
onOk={onJobFindModalOk}
onCancel={onJobModalCancel}
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
/>
<Row gutter={[16, 16]}>
<Col span={24}>

View File

@@ -4,35 +4,22 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
jobs(existingJobs = []) {
return existingJobs.filter(
(jobRef) => jobRef.__ref.includes(items) === false
);
},
},
});
}
export function JobsCloseExportButton({
bodyshop,
currentUser,
@@ -114,9 +101,6 @@ export function JobsCloseExportButton({
//Check to see if any of them failed. If they didn't don't execute the update.
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
const successfulTransactions = PartnerResponse.data.filter(
(r) => r.success
);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.forEach((ft) => {
@@ -175,15 +159,12 @@ export function JobsCloseExportButton({
},
});
if (!!!jobUpdateResponse.errors) {
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
@@ -192,31 +173,13 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => {
return selectedJobs.filter((i) => i !== jobId);
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -108,7 +108,7 @@ export function JobsConvertButton({
},
]}
>
<Select showSearch>
<Select>
{bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}

View File

@@ -145,13 +145,6 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_lost_sale")}
name="date_lost_sale"
>
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
</FormRow>
</div>
);

View File

@@ -18,14 +18,12 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID } from "../../graphql/appointments.queries";
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
@@ -50,10 +48,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobCosting" })),
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsDetailHeaderActions({
@@ -67,8 +61,6 @@ export function JobsDetailHeaderActions({
setJobCostingContext,
jobRO,
setTimeTicketContext,
setCardPaymentContext,
insertAuditTrail,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -145,73 +137,63 @@ export function JobsDetailHeaderActions({
<Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
>
{job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
t("menus.jobsactions.cancelallappointments")
) : (
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported,
},
<Popover
trigger="click"
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
content={
<Form
layout="vertical"
onFinish={async ({ lost_sale_reason }) => {
const jobUpdate = await cancelAllAppointments({
variables: {
jobid: job.id,
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
},
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
});
insertAuditTrail({
jobid: job.id,
operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason),
});
return;
}
}}
return;
}
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
)}
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
</Menu.Item>
<Menu.Item
disabled={
@@ -257,12 +239,7 @@ export function JobsDetailHeaderActions({
setTimeTicketContext({
actions: {},
context: {
jobId: job.id,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
context: { jobId: job.id },
});
}}
>
@@ -282,18 +259,6 @@ export function JobsDetailHeaderActions({
>
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Item
key="entercardpayments"
disabled={!job.converted}
onClick={() => {
setCardPaymentContext({
actions: {},
context: { jobid: job.id },
});
}}
>
{t("menus.header.entercardpayment")}
</Menu.Item>
<Menu.Item key="cccontract" disabled={jobRO || !job.converted}>
<Link
to={{

View File

@@ -27,8 +27,6 @@ export default async function DuplicateJob(
delete existingJob.id;
delete existingJob.createdat;
delete existingJob.updatedat;
delete existingJob.cieca_stl;
delete existingJob.cieca_ttl;
const newJob = {
...existingJob,
@@ -83,8 +81,6 @@ export async function CreateIouForJob(
delete existingJob.id;
delete existingJob.createdat;
delete existingJob.updatedat;
delete existingJob.cieca_stl;
delete existingJob.cieca_ttl;
const newJob = {
...existingJob,

View File

@@ -6,12 +6,14 @@ import BillsListTable from "../bills-list-table/bills-list-table.component";
import JobBillsTotal from "../job-bills-total/job-bills-total.component";
import PartsOrderListTableComponent from "../parts-order-list-table/parts-order-list-table.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
import PartsDispatchTable from "../parts-dispatch-table/parts-dispatch-table.component";
export default function JobsDetailPliComponent({
job,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick,
}) {
return (
<div>
@@ -43,6 +45,13 @@ export default function JobsDetailPliComponent({
billsQuery={billsQuery}
/>
</Col>
<Col span={24}>
<PartsDispatchTable
job={job}
handleOnRowClick={handlePartsDispatchOnRowClick}
billsQuery={billsQuery}
/>
</Col>
</Row>
</div>
);

View File

@@ -39,12 +39,24 @@ export default function JobsDetailPliContainer({ job }) {
}
};
const handlePartsDispatchOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsdispatchid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsdispatchid;
history.push({ search: queryString.stringify(search) });
}
};
return (
<JobsDetailPliComponent
job={job}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
/>
);
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
import { GenerateThumbUrl } from "./job-documents.utility";
function JobsDocumentGalleryExternal({
data,
@@ -15,7 +15,7 @@ function JobsDocumentGalleryExternal({
let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) {
acc.push({
fullsize: GenerateSrcUrl(value),
//src: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,

View File

@@ -52,7 +52,7 @@ function JobDocumentsLocalGalleryExternal({
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.push({ ...val, src: val.thumbnail, fullsize: val.src });
acc.push({ ...val, src: val.thumbnail });
}
return acc;
}, [])

View File

@@ -13,26 +13,12 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
jobs(existingJobs = []) {
return existingJobs.filter(
(jobRef) => jobRef.__ref.includes(items) === false
);
},
},
});
}
export function JobsExportAllButton({
bodyshop,
currentUser,
@@ -110,9 +96,7 @@ export function JobsExportAllButton({
Object.keys(groupedData).map(async (key) => {
//Check to see if any of them failed. If they didn't don't execute the update.
const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.forEach((ft) => {
@@ -171,17 +155,12 @@ export function JobsExportAllButton({
},
});
if (!!!jobUpdateResponse.errors) {
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
@@ -190,31 +169,14 @@ export function JobsExportAllButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
}
})
);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
setLoading(false);
};

View File

@@ -14,8 +14,6 @@ export default function JobsFindModalComponent({
importOptionsState,
modalSearchState,
jobsListRefetch,
partsQueueToggle,
setPartsQueueToggle,
}) {
const { t } = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState;
@@ -201,12 +199,6 @@ export default function JobsFindModalComponent({
>
{t("jobs.labels.override_header")}
</Checkbox>
<Checkbox
checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)}
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
</div>
);
}

View File

@@ -24,8 +24,6 @@ export default connect(
setSelectedJob,
importOptionsState,
modalSearchState,
partsQueueToggle,
setPartsQueueToggle,
...modalProps
}) {
const { t } = useTranslation();
@@ -93,8 +91,6 @@ export default connect(
jobsListRefetch={jobsList.refetch}
jobsList={jobsData}
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
/>
) : null}
</Modal>

View File

@@ -1,8 +1,8 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
BranchesOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
@@ -14,8 +14,8 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
@@ -46,7 +46,6 @@ export function JobsReadyList({ bodyshop }) {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: readyStatuses,
isConverted: true,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",

View File

@@ -231,14 +231,7 @@ export function LaborAllocationsTable({
{summary.adjustments.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
<Typography.Text
style={{
fontWeight: "bold",
color: summary.difference >= 0 ? "green" : "red",
}}
>
{summary.difference.toFixed(1)}
</Typography.Text>
{summary.difference.toFixed(1)}
</Table.Summary.Cell>
</Table.Summary.Row>
)}

View File

@@ -6,7 +6,6 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({
@@ -16,15 +15,6 @@ const mapStateToProps = createStructuredSelector({
function OwnerDetailJobsComponent({ bodyshop, owner }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -36,9 +26,6 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.vehicle"),
@@ -59,17 +46,11 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -79,9 +60,6 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -102,7 +80,6 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
scroll={{ x: true }}
rowKey="id"
dataSource={owner.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -8,11 +8,10 @@ export default function OwnerFindModalComponent({
setSelectedOwner,
ownersListLoading,
ownersList,
partsQueueToggle,
setPartsQueueToggle,
}) {
//setSelectedOwner is used to set the record id of the owner to use for adding the job.
const { t } = useTranslation();
const columns = [
{
title: t("owners.fields.ownr_ln"),
@@ -110,12 +109,6 @@ export default function OwnerFindModalComponent({
>
{t("owners.labels.create_new")}
</Checkbox>
<Checkbox
checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)}
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
</div>
);
}

View File

@@ -14,8 +14,6 @@ export default function OwnerFindModalContainer({
owner,
selectedOwner,
setSelectedOwner,
partsQueueToggle,
setPartsQueueToggle,
...modalProps
}) {
//use owner object to run query and find what possible owners there are.
@@ -61,8 +59,6 @@ export default function OwnerFindModalContainer({
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
ownersListLoading={ownersList.loading}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
ownersList={
ownersList.data && ownersList.data.search_owners
? ownersList.data.search_owners

View File

@@ -21,8 +21,7 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
useLazyQuery(SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -106,11 +106,7 @@ export default function OwnersListComponent({
<Input.Search
placeholder={search.search || t("general.labels.search")}
onSearch={(value) => {
if (value?.length >= 3) {
search.search = value;
} else {
delete search.search;
}
search.search = value;
history.push({ search: queryString.stringify(search) });
}}
enterButton

View File

@@ -0,0 +1,81 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Row, Table, notification } from "antd";
import moment from "moment-business-days";
import React from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function PartsDispatchExpander({ dispatch, job }) {
const { t } = useTranslation();
const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE);
const handleAccept = async ({ partsDispatchLineId }) => {
const accepted_at = moment();
const result = await updateDispatchLine({
variables: { id: partsDispatchLineId, line: { accepted_at } },
optimisticResponse: {
update_parts_dispatch_lines_by_pk: {
accepted_at,
id: partsDispatchLineId,
},
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("parts_dispatch.errors.accepting", {
error: JSON.stringify(result.errors),
}),
});
}
};
const columns = [
{
title: t("joblines.fields.part_qty"),
dataIndex: "quantity",
key: "quantity",
width: "10%",
//sorter: (a, b) => alphaSort(a.number, b.number),
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "joblineid",
key: "joblineid",
//sorter: (a, b) => alphaSort(a.number, b.number),
render: (text, record) => record.jobline.line_desc,
},
{
title: t("parts_dispatch_lines.fields.accepted_at"),
dataIndex: "accepted_at",
key: "accepted_at",
width: "20%",
//sorter: (a, b) => alphaSort(a.number, b.number),
render: (text, record) =>
record.accepted_at ? (
<DateTimeFormatter>{record.accepted_at}</DateTimeFormatter>
) : (
<Button
onClick={() => handleAccept({ partsDispatchLineId: record.id })}
>
{t("parts_dispatch.actions.accept")}
</Button>
),
},
];
return (
<Card>
<Row gutter={[16, 16]}>
<Col span={24}>
<Table
rowKey={"id"}
dataSource={dispatch.parts_dispatch_lines}
columns={columns}
/>
</Col>
</Row>
</Card>
);
}

View File

@@ -0,0 +1,155 @@
import {
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined,
} from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import PartsDispatchExpander from "../parts-dispatch-expander/parts-dispatch-expander.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartDispatchTableComponent({
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("job_special");
const { refetch } = billsQuery;
const recordActions = (record) => (
<Space wrap>
<PrintWrapperComponent
templateObject={{
name: Templates.parts_dispatch.key,
variables: { id: record.id },
}}
/>
</Space>
);
const columns = [
{
title: t("parts_dispatch.fields.number"),
dataIndex: "number",
key: "number",
sorter: (a, b) => alphaSort(a.number, b.number),
width: "10%",
sortOrder:
state.sortedInfo.columnKey === "number" && state.sortedInfo.order,
},
{
title: t("timetickets.fields.employee"),
dataIndex: "employeeid",
key: "employeeid",
sorter: (a, b) => alphaSort(a.employeeid, b.employeeid),
sortOrder:
state.sortedInfo.columnKey === "employeeid" && state.sortedInfo.order,
render: (text, record) => {
const e = bodyshop.employees.find((e) => e.id === record.employeeid);
return `${e?.first_name || ""} ${e?.last_name || ""}`.trim();
},
},
{
title: t("parts_dispatch.fields.percent_accepted"),
dataIndex: "percent_accepted",
key: "percent_accepted",
render: (text, record) =>
record.parts_dispatch_lines.length > 0
? `
${(
(record.parts_dispatch_lines.filter((l) => l.accepted_at)
.length /
record.parts_dispatch_lines.length) *
100
).toFixed(0)}%`
: "0%",
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
width: "10%",
render: (text, record) => recordActions(record, true),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Card
title={t("parts_dispatch.labels.parts_dispatch")}
extra={
<Space wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
>
<Table
loading={billsQuery.loading}
scroll={{
x: true, // y: "50rem"
}}
expandable={{
expandedRowRender: (record) => (
<PartsDispatchExpander dispatch={record} job={job} />
),
rowExpandable: (record) => true,
expandIcon: ({ expanded, onExpand, record }) =>
expanded ? (
<MinusCircleTwoTone onClick={(e) => onExpand(record, e)} />
) : (
<PlusCircleTwoTone onClick={(e) => onExpand(record, e)} />
),
}}
columns={columns}
rowKey="id"
dataSource={billsQuery.data ? billsQuery.data.parts_dispatch : []}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartDispatchTableComponent);

View File

@@ -113,8 +113,6 @@ export function PartsOrderListTableComponent({
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
};
}),
},

View File

@@ -79,20 +79,6 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.location")}
key={`${index}location`}

View File

@@ -1,39 +1,26 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { auth } from "../../firebase/firebase.utils";
import { UPDATE_BILLS } from "../../graphql/bills.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
import { logImEXEvent } from "../../firebase/firebase.utils";
import _ from "lodash";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updateBillCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
bills(existingJobs = []) {
return existingJobs.filter(
(billRef) => billRef.__ref.includes(items) === false
);
},
},
});
}
export function PayableExportAll({
bodyshop,
currentUser,
@@ -110,9 +97,7 @@ export function PayableExportAll({
proms.push(
(async () => {
const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.map((ft) =>
@@ -158,15 +143,7 @@ export function PayableExportAll({
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
@@ -179,11 +156,6 @@ export function PayableExportAll({
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache(
billUpdateResponse.data.update_bills.returning.map(
(bill) => bill.id
)
);
} else {
notification["error"]({
message: t("bills.errors.exporting", {
@@ -192,26 +164,6 @@ export function PayableExportAll({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
)
),
]);
}
}
})()
);
@@ -220,6 +172,8 @@ export function PayableExportAll({
await Promise.all(proms);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -4,35 +4,22 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { auth } from "../../firebase/firebase.utils";
import { UPDATE_BILLS } from "../../graphql/bills.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updateBillCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
bills(existingJobs = []) {
return existingJobs.filter(
(billRef) => billRef.__ref.includes(items) === false
);
},
},
});
}
export function PayableExportButton({
bodyshop,
currentUser,
@@ -172,11 +159,6 @@ export function PayableExportButton({
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache(
billUpdateResponse.data.update_bills.returning.map(
(bill) => bill.id
)
);
} else {
notification["error"]({
message: t("bills.errors.exporting", {
@@ -185,25 +167,7 @@ export function PayableExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
)
),
]);
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (setSelectedBills) {
setSelectedBills((selectedBills) => {

View File

@@ -1,186 +0,0 @@
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Descriptions,
InputNumber,
Modal,
Space,
notification,
} from "antd";
import axios from "axios";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
GET_REFUNDABLE_AMOUNT_BY_JOBID,
INSERT_PAYMENT_RESPONSE,
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
} from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
const { confirm } = Modal;
const openNotificationWithIcon = (type, t) => {
notification[type]({
message: t("job_payments.notifications.error.title"),
description: t("job_payments.notifications.error.description"),
});
};
const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
const [refundAmount, setRefundAmount] = useState(0);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
const { loading, error, data } = useQuery(
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
{
variables: {
paymentid: record.id,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
},
}
);
const { data: refundable_amount, refetch } = useQuery(
GET_REFUNDABLE_AMOUNT_BY_JOBID,
{
variables: {
jobid: record.jobid,
},
}
);
const insertPayments = async (payment_response, refund_response) => {
await insertPayment({
variables: {
paymentInput: {
amount: -refund_response.data.amount,
transactionid: payment_response.response.receiptelements.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,
date: moment(Date.now()),
},
},
update(cache, { data }) {
cache.modify({
id: cache.identify({
id: payment_response.jobid,
__typename: "jobs",
}),
fields: {
payments(payments) {
return [...data.insert_payments.returning, ...payments];
},
},
});
},
});
await insertPaymentResponse({
variables: {
paymentResponse: {
amount: -refund_response.data.amount,
bodyshopid: payment_response.bodyshopid,
paymentid: payment_response.paymentid,
jobid: payment_response.jobid,
declinereason: "Refund",
ext_paymentid: payment_response.ext_paymentid,
successful: true,
response: refund_response.data,
},
},
});
};
const showConfirm = (payment_response) => {
confirm({
title: "Do you want to refund payment?",
content:
"The payment will be refunded. Click OK to confirm and Cancel to dismiss.",
async onOk() {
const refundResponse = await axios.post("/intellipay/payment_refund", {
bodyshop,
amount: refundAmount,
paymentid: payment_response.ext_paymentid,
});
if (refundResponse.data.status < 0) {
openNotificationWithIcon("error", t);
return;
}
insertPayments(payment_response, refundResponse);
// refetch refundable amount
refetch();
},
onCancel() {},
});
};
if (loading) return null;
if (error) return <p>Error loading data. Please Reload</p>;
const payment_response = data.payment_response[0];
const max_refundable_amount =
refundable_amount?.payment_response_aggregate.aggregate.sum.amount;
return (
<div>
<Descriptions
title={t("job_payments.titles.descriptions")}
contentStyle={{ fontWeight: "600" }}
column={4}
>
<Descriptions.Item label={t("job_payments.titles.payer")}>
{record.payer}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.payername")}>
{payment_response?.response?.nameOnCard ?? ""}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.amount")}>
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.dateOfPayment")}>
{<DateTimeFormatter>{record.created_at}</DateTimeFormatter>}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.transactionid")}>
{record.transactionid}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
{payment_response?.response?.paymentreferenceid ?? ""}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>
{record.type}
</Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>
{record.paymentnum}
</Descriptions.Item>
{payment_response && (
<Descriptions.Item label={t("job_payments.titles.refundamount")}>
<Space>
<InputNumber
onChange={setRefundAmount}
max={max_refundable_amount}
min={0}
/>
<Button onClick={() => showConfirm(payment_response)}>
{t("job_payments.buttons.refundpayment")}
</Button>
</Space>
</Descriptions.Item>
)}
</Descriptions>
</div>
);
};
export default PaymentExpandedRowComponent;

View File

@@ -13,26 +13,12 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updatePaymentCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
payments(existingJobs = []) {
return existingJobs.filter(
(paymentRef) => paymentRef.__ref.includes(items) === false
);
},
},
});
}
export function PaymentExportButton({
bodyshop,
currentUser,
@@ -171,11 +157,6 @@ export function PaymentExportButton({
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache(
paymentUpdateResponse.data.update_payments.returning.map(
(payment) => payment.id
)
);
} else {
notification["error"]({
message: t("payments.errors.exporting", {
@@ -191,25 +172,7 @@ export function PaymentExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
)
),
]);
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!loadingCallback) loadingCallback(false);
setLoading(false);
};

View File

@@ -13,25 +13,11 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
function updatePaymentCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
payments(existingJobs = []) {
return existingJobs.filter(
(paymentRef) => paymentRef.__ref.includes(items) === false
);
},
},
});
}
export function PaymentsExportAllButton({
bodyshop,
currentUser,
@@ -39,7 +25,7 @@ export function PaymentsExportAllButton({
disabled,
loadingCallback,
completedCallback,
refetch,
refetch
}) {
const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -98,9 +84,7 @@ export function PaymentsExportAllButton({
proms.push(
(async () => {
const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.map((ft) =>
@@ -146,15 +130,7 @@ export function PaymentsExportAllButton({
});
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
paymentIdList: [key],
payment: {
exportedat: new Date(),
},
@@ -166,11 +142,6 @@ export function PaymentsExportAllButton({
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache(
paymentUpdateResponse.data.update_payments.returning.map(
(payment) => payment.id
)
);
} else {
notification["error"]({
message: t("payments.errors.exporting", {
@@ -179,26 +150,6 @@ export function PaymentsExportAllButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
)
),
]);
}
}
})()
);
@@ -206,6 +157,7 @@ export function PaymentsExportAllButton({
await Promise.all(proms);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -1,156 +0,0 @@
import { CopyFilled } from "@ant-design/icons";
import { Button, Form, Popover, Space, message } from "antd";
import axios from "axios";
import Dinero from "dinero.js";
import { parsePhoneNumber } from "libphonenumber-js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
openChatByPhone,
setMessage,
} from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PaymentsGenerateLink);
export function PaymentsGenerateLink({
bodyshop,
callback,
job,
openChatByPhone,
setMessage,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [paymentLink, setPaymentLink] = useState(null);
const handleFinish = async ({ amount }) => {
setLoading(true);
const p = parsePhoneNumber(job.ownr_ph1, "CA");
setLoading(true);
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: amount,
account: job.ro_number,
invoice: job.id,
});
setLoading(false);
setPaymentLink(response.data.shorUrl);
openChatByPhone({
phone_num: p.formatInternational(),
jobid: job.id,
});
setMessage(
t("payments.labels.smspaymentreminder", {
shopname: bodyshop.shopname,
amount: amount,
payment_link: response.data.shorUrl,
})
);
//Add in confirmation & errors.
if (callback) callback();
// setVisible(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("payments.fields.amount")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name="amount"
>
<CurrencyFormItemComponent />
</Form.Item>
{paymentLink && (
<Space direction="vertical">
<Space
style={{ cursor: "pointer" }}
onClick={() => {
navigator.clipboard.writeText(paymentLink);
message.success(t("general.actions.copied"));
}}
>
<div
onClick={() => {
//Copy the link.
}}
>
{paymentLink}
</div>{" "}
<CopyFilled />
</Space>
<Button
onClick={() => {
const p = parsePhoneNumber(job.ownr_ph1, "CA");
openChatByPhone({
phone_num: p.formatInternational(),
jobid: job.id,
});
setMessage(
t("payments.labels.smspaymentreminder", {
shopname: bodyshop.shopname,
amount: Dinero({
amount: Math.round(form.getFieldValue("amount") * 100),
}).toFormat(),
payment_link: paymentLink,
})
);
}}
>
{t("general.actions.sendbysms")}
</Button>
</Space>
)}
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button
onClick={() => {
form.resetFields();
setPaymentLink(null);
setVisible(false);
}}
>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
return (
<Popover content={popContent} visible={visible}>
<Button onClick={() => setVisible(true)} loading={loading}>
{t("payments.actions.generatepaymentlink")}
</Button>
</Popover>
);
}

View File

@@ -5,13 +5,11 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectPrintCenter } from "../../redux/modals/modals.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
@@ -24,7 +22,6 @@ export function PrintCenterItemComponent({
id,
bodyshop,
disabled,
technician,
}) {
const [loading, setLoading] = useState(false);
const { context } = printCenterModal;
@@ -47,24 +44,19 @@ export function PrintCenterItemComponent({
<Space wrap>
{item.title}
<PrinterOutlined onClick={renderToNewWindow} />
{!technician ? (
<MailOutlined
onClick={() => {
GenerateDocument(
{
name: item.key,
variables: { id: id },
},
{
to: context.job && context.job.ownr_ea,
subject: item.subject,
},
"e",
id
);
}}
/>
) : null}
<MailOutlined
onClick={() => {
GenerateDocument(
{
name: item.key,
variables: { id: id },
},
{ to: context.job && context.job.ownr_ea, subject: item.subject },
"e",
id
);
}}
/>
{loading && <Spin />}
</Space>
</li>

View File

@@ -9,9 +9,11 @@ export default function PrintWrapperComponent({
children,
id,
emailOnly = false,
disabled,
}) {
const [loading, setLoading] = useState(false);
const handlePrint = async (type) => {
if (disabled) return;
setLoading(true);
await GenerateDocument(templateObject, messageObject, type, id);
setLoading(false);
@@ -20,8 +22,18 @@ export default function PrintWrapperComponent({
return (
<Space>
{children || null}
{!emailOnly && <PrinterFilled onClick={() => handlePrint("p")} />}
<MailFilled onClick={() => handlePrint("e")} />
{!emailOnly && (
<PrinterFilled
disabled={disabled}
onClick={() => handlePrint("p")}
style={{ cursor: disabled ? "not-allowed" : null }}
/>
)}
<MailFilled
disabled={disabled}
onClick={() => handlePrint("e")}
style={{ cursor: disabled ? "not-allowed" : null }}
/>
{loading && <Spin />}
</Space>
);

View File

@@ -20,9 +20,9 @@ import ProductionBoardCard from "../production-board-kanban-card/production-boar
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
//import "@asseinfo/react-kanban/dist/styles.css";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -153,18 +153,6 @@ export function ProductionBoardKanbanComponent({
0
)
.toFixed(1);
const totalLAB = data
.reduce(
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0
)
.toFixed(1);
const totalLAR = data
.reduce(
(acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0
)
.toFixed(1);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
@@ -248,14 +236,6 @@ export function ProductionBoardKanbanComponent({
title={t("dashboard.titles.productionhours")}
value={totalHrs}
/>
<Statistic
title={t("dashboard.titles.labhours")}
value={totalLAB}
/>
<Statistic
title={t("dashboard.titles.larhours")}
value={totalLAR}
/>
<Statistic
title={t("appointments.labels.inproduction")}
value={data && data.length}

View File

@@ -455,8 +455,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
bodyshop.employees.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
@@ -474,8 +474,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
bodyshop.employees.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment
@@ -492,8 +492,8 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
bodyshop.employees.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr" />
@@ -508,9 +508,9 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_refinish)
bodyshop.employees.find((e) => e.id === a.employee_refinish)
?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)
bodyshop.employees.find((e) => e.id === b.employee_refinish)
?.first_name
),
render: (text, record) => (

View File

@@ -1,33 +1,31 @@
import { PrinterFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Descriptions, Drawer, PageHeader, Space } from "antd";
import { Descriptions, Drawer, Space, PageHeader, Button } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import StartChatButton from "../chat-open-button/chat-open-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import { PrinterFilled } from "@ant-design/icons";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
@@ -42,7 +40,6 @@ export function ProductionListDetail({
bodyshop,
jobs,
setPrintCenterContext,
technician,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
@@ -69,9 +66,7 @@ export function ProductionListDetail({
title={theJob.ro_number}
extra={
<Space wrap>
{!technician ? (
<ProductionRemoveButton jobId={theJob.id} />
) : null}
<ProductionRemoveButton jobId={theJob.id} />{" "}
<Button
onClick={() => {
setPrintCenterContext({
@@ -87,9 +82,7 @@ export function ProductionListDetail({
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
{!technician ? (
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
) : null}
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
</Space>
}
/>

View File

@@ -55,27 +55,25 @@ export function ProductionListPrint({ bodyshop }) {
<Menu.SubMenu
title={t("reportcenter.templates.production_by_technician_one")}
>
{bodyshop.employees
.filter((e) => e.active)
.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
{bodyshop.employees.map((e) => (
<Menu.Item
key={e.id}
onClick={async () => {
setLoading(true);
await GenerateDocument(
{
name: production_by_technician_one.key,
variables: { id: e.id },
},
{},
"p"
);
setLoading(false);
}}
>
{e.first_name} {e.last_name}
</Menu.Item>
))}
</Menu.SubMenu>
<Menu.SubMenu
title={t("reportcenter.templates.production_by_category_one")}

View File

@@ -184,18 +184,6 @@ export function ProductionListTable({
0
)
.toFixed(1);
const totalLAB = data
.reduce(
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0
)
.toFixed(1);
const totalLAR = data
.reduce(
(acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0
)
.toFixed(1);
return (
<div>
<PageHeader
@@ -205,14 +193,6 @@ export function ProductionListTable({
title={t("dashboard.titles.productionhours")}
value={totalHrs}
/>
<Statistic
title={t("dashboard.titles.labhours")}
value={totalLAB}
/>
<Statistic
title={t("dashboard.titles.larhours")}
value={totalLAR}
/>
<Statistic
title={t("appointments.labels.inproduction")}
value={dataSource && dataSource.length}

View File

@@ -13,29 +13,20 @@ import {
QUERY_APPOINTMENTS_BY_JOBID,
} from "../../graphql/appointments.queries";
import { QUERY_LBR_HRS_BY_PK, UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectSchedule } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormat } from "../../utils/DateFormatter";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
import ScheduleJobModalComponent from "./schedule-job-modal.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
scheduleModal: selectSchedule,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ScheduleJobModalContainer({
@@ -43,8 +34,6 @@ export function ScheduleJobModalContainer({
bodyshop,
toggleModalVisible,
setEmailOptions,
currentUser,
insertAuditTrail,
}) {
const { visible, context, actions } = scheduleModal;
const { jobId, job, previousEvent } = context;
@@ -133,22 +122,12 @@ export function ScheduleJobModalContainer({
end: moment(values.start).add(bodyshop.appt_length || 60, "minutes"),
color: values.color,
note: values.note,
created_by: currentUser.email,
},
jobId: jobId,
altTransport: values.alt_transport,
},
});
if (!appt.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.appointmentinsert(
DateTimeFormat(values.start)
),
});
}
if (!!appt.errors) {
notification["error"]({
message: t("appointments.errors.saving", {
@@ -170,7 +149,6 @@ export function ScheduleJobModalContainer({
scheduled_in: values.start,
scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
date_lost_sale: null,
},
},
});

View File

@@ -59,12 +59,11 @@ export function ScheduleManualEvent({ bodyshop, event }) {
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
}
form.resetFields();
setVisibility(false);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
setVisibility(false);
}
};

View File

@@ -8,45 +8,18 @@ export const CalculateWorkingDaysThisMonth = () => {
return moment().endOf("month").businessDaysIntoMonth();
};
export const CalculateWorkingDaysInPeriod = (start, end) => {
return moment(start).businessDiff(moment(end));
};
export const CalculateWorkingDaysAsOfToday = () => {
return moment().businessDaysIntoMonth();
};
export const CalculateWorkingDaysLastMonth = () => {
return moment().subtract(1, "month").endOf("month").businessDaysIntoMonth();
};
export const WeeklyTargetHrs = (dailyTargetHrs, bodyshop) => {
return (
dailyTargetHrs *
CalculateWorkingDaysInPeriod(
moment().startOf("week"),
moment().endOf("week")
)
);
};
export const WeeklyTargetHrsInPeriod = (
dailyTargetHrs,
start,
end,
bodyshop
) => {
return dailyTargetHrs * CalculateWorkingDaysInPeriod(start, end);
return dailyTargetHrs * 5;
};
export const MonthlyTargetHrs = (dailyTargetHrs, bodyshop) => {
return dailyTargetHrs * CalculateWorkingDaysThisMonth();
};
export const LastMonthTargetHrs = (dailyTargetHrs, bodyshop) => {
return dailyTargetHrs * CalculateWorkingDaysLastMonth();
};
export const AsOfTodayTargetHrs = (dailyTargetHrs, bodyshop) => {
return dailyTargetHrs * CalculateWorkingDaysAsOfToday();
};

View File

@@ -1,26 +0,0 @@
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div
style={{
backgroundColor: "white",
border: "1px solid gray",
padding: "0.5rem",
}}
>
<p style={{ margin: "0" }}>{label}</p>
{payload.map((data, index) => {
return (
<p style={{ margin: "10px 0", color: data.color }} key={index}>{`${
data.name
} : ${data.value.toFixed(1)}`}</p>
);
})}
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@@ -1,54 +0,0 @@
import { Card } from "antd";
import React from "react";
import {
Area,
CartesianGrid,
ComposedChart,
Legend,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import CustomTooltip from "./chart-custom-tooltip";
const graphProps = {
strokeWidth: 3,
};
export default function ScoreboardTimeTicketsChart({ data, chartTitle }) {
return (
<Card title={chartTitle}>
<ResponsiveContainer width="100%" height={275}>
<ComposedChart
data={data}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis yAxisId="left" strokeWidth={graphProps.strokeWidth} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
name="Target Hours"
type="monotone"
dataKey="accTargetHrs"
stroke="#ff7300"
yAxisId="left"
strokeWidth={graphProps.strokeWidth}
/>
<Area
type="monotone"
name="MTD Hours"
dataKey="accHrs"
fill="lightblue"
stroke="blue"
yAxisId="left"
/>
</ComposedChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -1,393 +0,0 @@
import { useQuery } from "@apollo/client";
import { Col, Row } from "antd";
import _ from "lodash";
import moment from "moment";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TIME_TICKETS_IN_RANGE_SB } from "../../graphql/timetickets.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import ScoreboardTimeTicketsChart from "./scoreboard-timetickets.chart.component";
import ScoreboardTicketsStats from "./scoreboard-timetickets.stats.component";
import ScoreboardTimeticketsTargetsTable from "./scoreboard-timetickets.targets-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardTimeTicketsStats);
export function ScoreboardTimeTicketsStats({ bodyshop }) {
const { t } = useTranslation();
const startDate = moment().startOf("month")
const endDate = moment().endOf("month");
const fixedPeriods = useMemo(() => {
const endOfThisMonth = moment().endOf("month");
const startofthisMonth = moment().startOf("month");
const endOfLastmonth = moment().subtract(1, "month").endOf("month");
const startOfLastmonth = moment().subtract(1, "month").startOf("month");
const endOfThisWeek = moment().endOf("week");
const startOfThisWeek = moment().startOf("week");
const endOfLastWeek = moment().subtract(1, "week").endOf("week");
const startOfLastWeek = moment().subtract(1, "week").startOf("week");
const endOfPriorWeek = moment().subtract(2, "week").endOf("week");
const startOfPriorWeek = moment().subtract(2, "week").startOf("week");
const allDates = [
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
];
const start = moment.min(allDates);
const end = moment.max(allDates);
return {
start,
end,
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
endOfPriorWeek,
startOfPriorWeek,
};
}, []);
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE_SB, {
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
skip: !fixedPeriods,
});
const calculatedData = useMemo(() => {
if (!data) return [];
const ret = {
totalThisWeek: 0,
totalThisWeekLAB: 0,
totalThisWeekLAR: 0,
totalLastWeek: 0,
totalLastWeekLAB: 0,
totalLastWeekLAR: 0,
totalPriorWeek: 0,
totalPriorWeekLAB: 0,
totalPriorWeekLAR: 0,
totalThisMonth: 0,
totalThisMonthLAB: 0,
totalThisMonthLAR: 0,
totalLastMonth: 0,
totalLastMonthLAB: 0,
totalLastMonthLAR: 0,
actualTotalOverPeriod: 0,
actualTotalOverPeriodLAB: 0,
actualTotalOverPeriodLAR: 0,
totalEffieciencyOverPeriod: 0,
totalEffieciencyOverPeriodLAB: 0,
totalEffieciencyOverPeriodLAR: 0,
seperatedThisWeek: {
sunday: {
total: 0,
lab: 0,
lar: 0,
},
monday: {
total: 0,
lab: 0,
lar: 0,
},
tuesday: {
total: 0,
lab: 0,
lar: 0,
},
wednesday: {
total: 0,
lab: 0,
lar: 0,
},
thursday: {
total: 0,
lab: 0,
lar: 0,
},
friday: {
total: 0,
lab: 0,
lar: 0,
},
saturday: {
total: 0,
lab: 0,
lar: 0,
},
},
};
data.fixedperiod.forEach((ticket) => {
const ticketDate = moment(ticket.date);
if (
ticketDate.isBetween(
fixedPeriods.startOfThisWeek,
fixedPeriods.endOfThisWeek,
undefined,
"[]"
)
) {
ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalThisWeekLAB = ret.totalThisWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalThisWeekLAR = ret.totalThisWeekLAR + ticket.productivehrs;
//Seperate out to Day of Week
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].total =
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].total + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].lab =
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].lar =
ret.seperatedThisWeek[
moment(ticket.date).format("dddd").toLowerCase()
].lar + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastWeek,
fixedPeriods.endOfLastWeek,
undefined,
"[]"
)
) {
ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastWeekLAB = ret.totalLastWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastWeekLAR = ret.totalLastWeekLAR + ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfPriorWeek,
fixedPeriods.endOfPriorWeek,
undefined,
"[]"
)
) {
ret.totalPriorWeek = ret.totalPriorWeek + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalPriorWeekLAB = ret.totalPriorWeekLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalPriorWeekLAR = ret.totalPriorWeekLAR + ticket.productivehrs;
}
if (
ticketDate.isBetween(
fixedPeriods.startofthisMonth,
fixedPeriods.endOfThisMonth,
undefined,
"[]"
)
) {
ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs;
ret.actualTotalOverPeriod =
ret.actualTotalOverPeriod + (ticket.actualhrs || 0);
if (ticket.ciecacode !== "LAR") {
ret.totalThisMonthLAB = ret.totalThisMonthLAB + ticket.productivehrs;
ret.actualTotalOverPeriodLAB =
ret.actualTotalOverPeriodLAB + (ticket.actualhrs || 0);
}
if (ticket.ciecacode === "LAR") {
ret.totalThisMonthLAR = ret.totalThisMonthLAR + ticket.productivehrs;
ret.actualTotalOverPeriodLAR =
ret.actualTotalOverPeriodLAR + (ticket.actualhrs || 0);
}
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastmonth,
fixedPeriods.endOfLastmonth,
undefined,
"[]"
)
) {
ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
ret.totalLastMonthLAB = ret.totalLastMonthLAB + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
ret.totalLastMonthLAR = ret.totalLastMonthLAR + ticket.productivehrs;
}
});
ret.totalEffieciencyOverPeriod = ret.actualTotalOverPeriod
? (ret.totalThisMonth / ret.actualTotalOverPeriod) * 100
: 0;
ret.totalEffieciencyOverPeriodLAB = ret.actualTotalOverPeriodLAB
? (ret.totalThisMonthLAB / ret.actualTotalOverPeriodLAB) * 100
: 0;
ret.totalEffieciencyOverPeriodLAR = ret.actualTotalOverPeriodLAR
? (ret.totalThisMonthLAR / ret.actualTotalOverPeriodLAR) * 100
: 0;
roundObject(ret);
const ticketsGroupedByDate = _.groupBy(data.timetickets, "date");
const listOfDays = Utils.ListOfDaysInCurrentMonth();
const combinedData = [],
labData = [],
larData = [];
var acc_comb = 0;
var acc_lab = 0;
var acc_lar = 0;
listOfDays.forEach((day) => {
const r = {
date: moment(day).format("MM/DD"),
actualhrs: 0,
productivehrs: 0,
};
const combined = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
day
) +
(bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget),
1
),
accHrs: 0,
};
const lab = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget,
day
) + bodyshop.scoreboard_target.dailyBodyTarget,
1
),
accHrs: 0,
};
const lar = {
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyPaintTarget,
day
) + bodyshop.scoreboard_target.dailyPaintTarget,
1
),
accHrs: 0,
};
if (ticketsGroupedByDate[day]) {
ticketsGroupedByDate[day].forEach((ticket) => {
r.actualhrs = r.actualhrs + ticket.actualhrs;
r.productivehrs = r.productivehrs + ticket.productivehrs;
acc_comb = acc_comb + ticket.productivehrs;
if (ticket.ciecacode !== "LAR")
acc_lab = acc_lab + ticket.productivehrs;
if (ticket.ciecacode === "LAR")
acc_lar = acc_lar + ticket.productivehrs;
});
}
combined.accHrs = acc_comb;
lab.accHrs = acc_lab;
lar.accHrs = acc_lar;
combinedData.push({ ...r, ...combined });
labData.push({ ...r, ...lab });
larData.push({ ...r, ...lar });
});
return {
fixed: ret,
combinedData: combinedData,
labData: labData,
larData: larData,
};
}, [fixedPeriods, data, bodyshop]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<ScoreboardTimeticketsTargetsTable />
</Col>
<Col span={24}>
<ScoreboardTicketsStats data={calculatedData.fixed} />
</Col>
<Col span={24}>
<ScoreboardTimeTicketsChart
data={calculatedData.combinedData}
chartTitle={t("scoreboard.labels.combinedcharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.labData}
chartTitle={t("scoreboard.labels.bodycharttitle")}
/>
</Col>
<Col span={12}>
<ScoreboardTimeTicketsChart
data={calculatedData.larData}
chartTitle={t("scoreboard.labels.refinishcharttitle")}
/>
</Col>
</Row>
);
}
function roundObject(inputObj) {
for (var key of Object.keys(inputObj)) {
if (typeof inputObj[key] === "number") {
inputObj[key] = inputObj[key].toFixed(1);
} else if (Array.isArray(inputObj[key])) {
inputObj[key].forEach((item) => roundObject(item));
} else if (typeof inputObj[key] === "object") {
roundObject(inputObj[key]);
}
}
}

View File

@@ -1,617 +0,0 @@
import {
Card,
Col,
Form,
Row,
Space,
Statistic,
Switch,
Typography,
} from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Util from "../scoreboard-targets-table/scoreboard-targets-table.util";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardTicketsStats);
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(storedValue));
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
export function ScoreboardTicketsStats({ data, bodyshop }) {
const { t } = useTranslation();
const [isLarge, setIsLarge] = useLocalStorage("isLargeStatistic", false);
const statisticSize = isLarge ? 36 : 24;
const statisticWeight = isLarge ? 550 : "normal";
const daySpan =
Util.CalculateWorkingDaysInPeriod(
moment().startOf("week"),
moment().endOf("week")
) > 5
? 3
: 4;
return (
<Card
title={t("scoreboard.labels.productivestatistics")}
extra={
<Form.Item
label={t("general.labels.tvmode")}
valuePropName="checked"
name={["tvmode"]}
>
<Switch
onClick={() => setIsLarge(!isLarge)}
defaultChecked={isLarge}
/>
</Form.Item>
}
>
<Row gutter={[16, 16]}>
<Col md={24}>
{/* Daily Stats */}
<Space direction="vertical" size="middle" style={{ display: "flex" }}>
<Row gutter={[16, 16]} align="center">
{[
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
].map((day) => {
if (bodyshop.workingdays[day] === true) {
return (
<Col key={day} span={daySpan} align="center">
<Card size="small" title={t("general.labels." + day)}>
<Row gutter={[8, 8]}>
<Col span={24}>
<Statistic
value={data.seperatedThisWeek[day].total}
valueStyle={{
color:
parseFloat(
data.seperatedThisWeek[day].total
) >=
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.seperatedThisWeek[day].lab}
valueStyle={{
color:
parseFloat(data.seperatedThisWeek[day].lab) >=
bodyshop.scoreboard_target.dailyBodyTarget
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.seperatedThisWeek[day].lar}
valueStyle={{
color:
parseFloat(data.seperatedThisWeek[day].lar) >=
bodyshop.scoreboard_target.dailyPaintTarget
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
);
} else {
return null;
}
})}
</Row>
{/* Weekly Stats */}
<Row gutter={[16, 16]}>
{/* This Week */}
<Col span={8} align="center">
<Card size="small" title={t("scoreboard.labels.thisweek")}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={data.totalThisWeek}
valueStyle={{
color:
parseFloat(data.totalThisWeek) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.totalThisWeekLAB}
valueStyle={{
color:
parseFloat(data.totalThisWeekLAB) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.totalThisWeekLAR}
valueStyle={{
color:
parseFloat(data.totalThisWeekLAR) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
{/* Last Week */}
<Col span={8} align="center">
<Card size="small" title={t("scoreboard.labels.lastweek")}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={data.totalLastWeek}
valueStyle={{
color:
parseFloat(data.totalLastWeek) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.totalLastWeekLAB}
valueStyle={{
color:
parseFloat(data.totalLastWeekLAB) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.totalLastWeekLAR}
valueStyle={{
color:
parseFloat(data.totalLastWeekLAR) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
{/* Prior Week */}
<Col span={8} align="center">
<Card size="small" title={t("scoreboard.labels.priorweek")}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={data.totalPriorWeek}
valueStyle={{
color:
parseFloat(data.totalPriorWeek) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.totalPriorWeekLAB}
valueStyle={{
color:
parseFloat(data.totalPriorWeekLAB) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.totalPriorWeekLAR}
valueStyle={{
color:
parseFloat(data.totalPriorWeekLAR) >=
Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
</Row>
{/* Monthly Stats */}
<Row gutter={[16, 16]}>
{/* This Month */}
<Col span={8} align="center">
<Card size="small" title={t("scoreboard.labels.thismonth")}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={data.totalThisMonth}
valueStyle={{
color:
parseFloat(data.totalThisMonth) >=
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.totalThisMonthLAB}
valueStyle={{
color:
parseFloat(data.totalThisMonthLAB) >=
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.totalThisMonthLAR}
valueStyle={{
color:
parseFloat(data.totalThisMonthLAR) >=
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
{/* Last Month */}
<Col span={8} align="center">
<Card size="small" title={t("scoreboard.labels.lastmonth")}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={data.totalLastMonth}
valueStyle={{
color:
parseFloat(data.totalLastMonth) >=
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={data.totalLastMonthLAB}
valueStyle={{
color:
parseFloat(data.totalLastMonthLAB) >=
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={data.totalLastMonthLAR}
valueStyle={{
color:
parseFloat(data.totalLastMonthLAR) >=
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
? "green"
: "red",
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
{/* Efficiency Over Period */}
<Col span={8} align="center">
<Card
size="small"
title={t("scoreboard.labels.efficiencyoverperiod")}
>
<Row gutter={[16, 16]}>
<Col span={24}>
<Statistic
value={`${data.totalEffieciencyOverPeriod || 0}%`}
valueStyle={{
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.body")}
</Typography.Text>
}
value={`${data.totalEffieciencyOverPeriodLAB || 0}%`}
valueStyle={{
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
<Col span={12}>
<Statistic
title={
<Typography.Text strong>
{t("scoreboard.labels.refinish")}
</Typography.Text>
}
value={`${data.totalEffieciencyOverPeriodLAR || 0}%`}
valueStyle={{
fontSize: statisticSize,
fontWeight: statisticWeight,
}}
/>
</Col>
</Row>
</Card>
</Col>
</Row>
</Space>
{/* Disclaimer */}
<Typography.Text type="secondary">
*{t("scoreboard.labels.calendarperiod")}
</Typography.Text>
</Col>
</Row>
</Card>
);
}

View File

@@ -1,285 +0,0 @@
import { CalendarOutlined } from "@ant-design/icons";
import { Card, Col, Divider, Row, Statistic } from "antd";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Util from "../scoreboard-targets-table/scoreboard-targets-table.util";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const rowGutter = [16, 16];
const statSpans = { xs: 24, sm: 3 };
export function ScoreboardTimeticketsTargetsTable({ bodyshop }) {
const { t } = useTranslation();
return (
<Card title={t("scoreboard.labels.targets")}>
<Row gutter={rowGutter}>
<Col xs={24} sm={{ offset: 0, span: 3 }} lg={{ span: 3 }}>
<Statistic
title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined />}
/>
</Col>
<Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 20 }}>
<Row>
<Col {...statSpans}>
<Statistic title="Type" value={t("scoreboard.labels.body")} />
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.thisweek")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.lastweek")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.priorweek")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.thismonth")}
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.lastmonth")}
value={Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
</Row>
<Row>
<Col {...statSpans}>
<Statistic value={t("scoreboard.labels.refinish")} />
</Col>
<Col {...statSpans}>
<Statistic value={bodyshop.scoreboard_target.dailyPaintTarget} />
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrsInPeriod(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().startOf("week"),
moment().endOf("week"),
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(1, "week").startOf("week"),
moment().subtract(1, "week").endOf("week"),
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
moment().subtract(2, "week").startOf("week"),
moment().subtract(2, "week").endOf("week"),
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.LastMonthTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
</Row>
</Col>
</Row>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardTimeticketsTargetsTable);

View File

@@ -42,7 +42,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
bodyshop && bodyshop.imexshopid
);
return (
<div>
<LayoutFormRow
@@ -681,13 +680,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
>
<Select mode="tags" />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
name={["md_functionality_toggles","parts_queue_toggle"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={["last_name_first"]}
label={t("bodyshop.fields.last_name_first")}

View File

@@ -1,26 +1,21 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification, Space } from "antd";
import axios from "axios";
import moment from "moment";
import momenttz from "moment-timezone";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TechClockInComponent from "./tech-job-clock-in-form.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import moment from "moment";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) =>
@@ -30,10 +25,9 @@ export function TechClockInContainer({
setTimeTicketContext,
technician,
bodyshop,
currentUser,
}) {
console.log(
"🚀 ~ file: tech-job-clock-in-form.container.jsx:30 ~ technician:",
"🚀 ~ file: tech-job-clock-in-form.container.jsx:29 ~ technician:",
technician
);
const [form] = Form.useForm();
@@ -50,20 +44,14 @@ export function TechClockInContainer({
const handleFinish = async (values) => {
setLoading(true);
const theTime = (await axios.post("/utils/time")).data;
const result = await insertTimeTicket({
variables: {
timeTicketInput: [
{
bodyshopid: bodyshop.id,
employeeid: technician.id,
date:
typeof bodyshop.timezone === "string"
? momenttz.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD")
: typeof bodyshop.timezone === "number"
? moment(theTime)
.format("YYYY-MM-DD")
.utcOffset(bodyshop.timezone)
: moment(theTime).format("YYYY-MM-DD"),
date: moment(theTime).format("YYYY-MM-DD"),
clockon: moment(theTime),
jobid: values.jobid,
cost_center: values.cost_center,
@@ -78,12 +66,6 @@ export function TechClockInContainer({
values.cost_center
);
}),
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(" ", technician.first_name, " ", technician.last_name)
.trim()
),
},
],
},
@@ -118,24 +100,13 @@ export function TechClockInContainer({
employeeid: technician.id,
flat_rate: emps.flat_rate,
},
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
),
},
});
}}
>
{t("timetickets.actions.enter")}
</Button>
<TechJobPrintTickets attendacePrint={false} />
<TechJobPrintTickets />
<Button
type="primary"
onClick={() => form.submit()}

View File

@@ -1,4 +1,4 @@
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import { Button, Card, DatePicker, Form, Popover, Space } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -21,13 +21,12 @@ export default connect(
mapDispatchToProps
)(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
export function TechJobPrintTickets({ technician, event }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const Templates = TemplateList("report_center");
useEffect(() => {
if (visibility && event) {
@@ -45,10 +44,7 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) {
try {
await GenerateDocument(
{
name:
attendacePrint === true
? Templates.attendance_employee.key
: Templates.timetickets_employee.key,
name: TemplateList().timetickets_employee.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
@@ -64,12 +60,9 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) {
},
{
to: technician.email,
subject:
attendacePrint === true
? Templates.attendance_employee.subject
: Templates.timetickets_employee.subject,
subject: TemplateList().timetickets_employee.subject,
},
values.sendby // === "email" ? "e" : "p"
"p"
);
} catch (error) {
console.log(error);
@@ -99,25 +92,10 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) {
format={"MM/DD/YYYY"}
/>
</Form.Item>
<Form.Item dependencies={["dates"]}>
{() => {
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="p"
>
<Radio.Group>
<Radio value="e">{t("general.labels.email")}</Radio>
<Radio value="p">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<Space wrap>
<Button type="primary" onClick={() => form.submit()}>
{t("reportcenter.actions.generate")}
{t("general.actions.print")}
</Button>
<Button
onClick={() => {
@@ -140,7 +118,7 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) {
return (
<Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick}>
{t("general.labels.reports")}
{t("general.actions.print")}
</Button>
</Popover>
);

View File

@@ -25,7 +25,6 @@ export function TechLookupJobsList({ bodyshop }) {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
isConverted: true,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",

View File

@@ -1,4 +1,8 @@
import Icon, { SearchOutlined, ScheduleOutlined } from "@ant-design/icons";
import Icon, {
SearchOutlined,
ScheduleOutlined,
CarOutlined,
} from "@ant-design/icons";
import { Layout, Menu } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -68,6 +72,15 @@ export function TechSider({ technician, techLogout }) {
<Menu.Item key="5" disabled={!!!technician} icon={<ScheduleOutlined />}>
<Link to={`/tech/list`}>{t("menus.tech.productionlist")}</Link>
</Menu.Item>
<Menu.Item
key="dispatchedparts"
disabled={!!!technician}
icon={<CarOutlined />}
>
<Link to={`/tech/dispatchedparts`}>
{t("menus.tech.dispatchedparts")}
</Link>
</Menu.Item>
<Menu.Item
key="6"
disabled={!!!technician}

View File

@@ -9,10 +9,9 @@ import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, {
HasRbacAccess,
@@ -21,7 +20,6 @@ import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -31,7 +29,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
export function TimeTicketList({
bodyshop,
authLevel,
currentUser,
disabled,
loading,
timetickets,
@@ -196,15 +193,7 @@ export function TimeTicketList({
}
},
},
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder:
state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by,
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
@@ -265,12 +254,7 @@ export function TimeTicketList({
(techConsole ? null : (
<TimeTicketEnterButton
actions={{ refetch }}
context={{
jobId: jobId,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
}}
context={{ jobId: jobId }}
disabled={disabled}
>
{t("timetickets.actions.enter")}

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, Modal, PageHeader, Space, notification } from "antd";
import { Button, Form, Modal, notification, PageHeader, Space } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -77,7 +77,6 @@ export function TimeTicketModalContainer({
)[0].rate
: null,
bodyshopid: bodyshop.id,
created_by: timeTicketModal.context.created_by,
},
],
},

View File

@@ -4,67 +4,48 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import DataLabel from "../data-label/data-label.component";
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
export default function TimeTicketShiftActive({
timetickets,
refetch,
isTechConsole,
}) {
export default function TimeTicketShiftActive({ timetickets, refetch }) {
const { t } = useTranslation();
return (
<div>
{timetickets.length > 0 ? (
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
height: "100%",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
{isTechConsole ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</div>
<div style={{ flexGrow: 1 }}>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
</div>
<div>
<Typography.Title level={2}>
{t("timetickets.labels.shiftalreadyclockedon")}
</Typography.Title>
<List
grid={{
gutter: 32,
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
}}
dataSource={timetickets || []}
renderItem={(ticket) => (
<List.Item>
<Card
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
</div>
) : null}
</div>

View File

@@ -1,8 +1,7 @@
import { useMutation } from "@apollo/client";
import { Button, Form, Space, notification } from "antd";
import { Button, Form, notification } from "antd";
import axios from "axios";
import moment from "moment";
import momenttz from "moment-timezone";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -13,7 +12,6 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TimeTicektShiftComponent from "./time-ticket-shift-form.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -31,10 +29,6 @@ export function TimeTicektShiftContainer({
isTechConsole,
checkIfAlreadyClocked,
}) {
console.log(
"🚀 ~ file: time-ticket-shift-form.container.jsx:28 ~ technician:",
technician
);
const [form] = Form.useForm();
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
const { t } = useTranslation();
@@ -69,30 +63,8 @@ export function TimeTicektShiftContainer({
employeeid: isTechConsole ? technician.id : employeeId,
cost_center: "timetickets.labels.shift",
clockon: theTime,
date:
typeof bodyshop.timezone === "string"
? momenttz.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD")
: typeof bodyshop.timezone === "number"
? moment(theTime)
.utcOffset(bodyshop.timezone)
.format("YYYY-MM-DD")
: moment(theTime).format("YYYY-MM-DD"),
date: theTime,
memo: values.memo,
created_by: isTechConsole
? currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
)
: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
],
},
@@ -126,14 +98,9 @@ export function TimeTicektShiftContainer({
initialValues={{ cost_center: t("timetickets.labels.shift") }}
>
<TimeTicektShiftComponent form={form} />
<Space wrap>
<Button htmlType="submit" loading={loading} type="primary">
{t("timetickets.actions.clockin")}
</Button>
{isTechConsole === true ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</Space>
<Button htmlType="submit" loading={loading}>
{t("timetickets.actions.clockin")}
</Button>
</Form>
</div>
);

View File

@@ -76,7 +76,6 @@ export function TimeTicketShiftContainer({
<TimeTicketShiftActive
timetickets={data ? data.timetickets : []}
refetch={refetch}
isTechConsole={isTechConsole}
/>
) : (
<TimeTicketShiftFormContainer

View File

@@ -6,9 +6,8 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -17,14 +16,6 @@ const mapStateToProps = createStructuredSelector({
export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
@@ -37,9 +28,6 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
{record.ro_number || t("general.labels.na")}
</Link>
),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
},
{
title: t("jobs.fields.owner"),
@@ -55,17 +43,11 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
{
@@ -75,9 +57,6 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
},
];
@@ -97,7 +76,6 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
rowKey="id"
scroll={{ x: true }}
dataSource={vehicle.jobs}
onChange={handleTableChange}
rowSelection={{
onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -16,12 +16,13 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
SEARCH_VEHICLES_FOR_AUTOCOMPLETE
);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
useLazyQuery(SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE);
const [
callIdSearch,
{ loading: idLoading, error: idError, data: idData },
] = useLazyQuery(SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE);
const executeSearch = (v) => {
if (v && v.variables?.search !== "" && v.variables.search.length >= 2)
callSearch(v);
callSearch(v);
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);

View File

@@ -154,6 +154,7 @@ export function VendorsFormComponent({
label={t("vendors.fields.phone")}
name="phone"
rules={[
{ required: true, message: t("general.validation.required") },
({ getFieldValue }) =>
PhoneItemFormatterValidation(getFieldValue, "phone"),
]}

View File

@@ -271,7 +271,6 @@ export const CANCEL_APPOINTMENTS_BY_JOB_ID = gql`
scheduled_completion
status
lost_sale_reason
date_lost_sale
}
}
`;

View File

@@ -5,7 +5,6 @@ export const INSERT_NEW_BILL = gql`
insert_bills(objects: $bill) {
returning {
id
invoice_number
}
}
}
@@ -25,11 +24,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
$limit: Int
$order: [bills_order_by!]!
) {
bills(
offset: $offset
limit: $limit
order_by: $order
) {
bills(offset: $offset, limit: $limit, order_by: $order) {
id
vendorid
vendor {
@@ -98,6 +93,23 @@ export const QUERY_BILLS_BY_JOBID = gql`
comments
user_email
}
parts_dispatch(where: { jobid: { _eq: $jobid } }) {
id
dispatched_at
dispatched_by
employeeid
number
parts_dispatch_lines {
joblineid
id
quantity
accepted_at
jobline {
id
line_desc
}
}
}
bills(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
id
vendorid

View File

@@ -39,7 +39,6 @@ export const QUERY_BODYSHOP = gql`
logo_img_path
md_ro_statuses
md_order_statuses
md_functionality_toggles
shopname
state
state_tax_id
@@ -159,7 +158,6 @@ export const UPDATE_SHOP = gql`
logo_img_path
md_ro_statuses
md_order_statuses
md_functionality_toggles
shopname
state
state_tax_id

View File

@@ -57,7 +57,6 @@ export const GET_LINE_TICKET_BY_PK = gql`
actualhrs
ciecacode
cost_center
created_by
date
id
jobid
@@ -219,7 +218,6 @@ export const GET_JOB_LINES_TO_ENTER_BILL = gql`
id
ro_number
}
v_make_desc
}
}
`;

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