Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Richer
c8f8a86a98 - Migrations for remind_at_sent
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-04-16 15:45:56 -04:00
40 changed files with 1110 additions and 1455 deletions

View File

@@ -134,6 +134,10 @@ jobs:
workflows: workflows:
deploy_and_build: deploy_and_build:
jobs: jobs:
- api-deploy:
filters:
branches:
only: master
- app-build: - app-build:
filters: filters:
branches: branches:

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2"> <babeledit_project version="1.2" be_version="2.7.1">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -3540,27 +3540,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>nobilllines</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> <concept_node>
<name>noneselected</name> <name>noneselected</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -3645,27 +3624,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>returnfrombill</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> <concept_node>
<name>savewithdiscrepancy</name> <name>savewithdiscrepancy</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -13828,27 +13786,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>unavailable</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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -14539,27 +14476,6 @@
<folder_node> <folder_node>
<name>titles</name> <name>titles</name>
<children> <children>
<concept_node>
<name>joblifecycle</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> <concept_node>
<name>labhours</name> <name>labhours</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -18185,27 +18101,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>media</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> <concept_node>
<name>message</name> <name>message</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -20097,48 +19992,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>human_readable</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>percentage</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> <concept_node>
<name>relative_end</name> <name>relative_end</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -20202,48 +20055,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>status</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>status_count</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> <concept_node>
<name>value</name> <name>value</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -20270,27 +20081,6 @@
<folder_node> <folder_node>
<name>content</name> <name>content</name>
<children> <children>
<concept_node>
<name>calculated_based_on</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> <concept_node>
<name>current_status_accumulated_time</name> <name>current_status_accumulated_time</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -20333,48 +20123,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>joblifecycle</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>jobs_in_since</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> <concept_node>
<name>legend_title</name> <name>legend_title</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -20571,53 +20319,6 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>titles</name>
<children>
<concept_node>
<name>dashboard</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>top_durations</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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -20830,27 +20531,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>hint</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> <concept_node>
<name>payer</name> <name>payer</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -31333,27 +31013,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>labor_hrs</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> <concept_node>
<name>labor_rates_subtotal</name> <name>labor_rates_subtotal</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -39624,27 +39283,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>paymentupdate</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> <concept_node>
<name>stripe</name> <name>stripe</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -45736,48 +45374,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>job_lifecycle_date_detail</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>job_lifecycle_date_summary</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> <concept_node>
<name>jobs_completed_not_invoiced</name> <name>jobs_completed_not_invoiced</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -16,28 +16,42 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions"; import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors"; import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route"; import PrivateRoute from "../utils/private-route";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/handleBeta";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page")); const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container")); const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline, online: selectOnline,
bodyshop: selectBodyshop bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)) setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline }) { export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useClient(); const client = useClient();
useEffect(() => { useEffect(() => {
@@ -94,6 +108,8 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
/> />
); );
handleBeta();
return ( return (
<Switch> <Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}> <Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
@@ -113,16 +129,32 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
<Route exact path="/disclaimer" component={DisclaimerPage} /> <Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<Route exact path="/mp/:paymentIs" component={MobilePaymentContainer} /> <Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/manage" component={ManagePage} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/tech" component={TechPageContainer} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary> </ErrorBoundary>
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} path="/edit" component={DocumentEditorContainer} /> <PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary> </ErrorBoundary>
</Suspense> </Suspense>
</Switch> </Switch>

View File

@@ -164,7 +164,6 @@ export function BillDetailEditcontainer({
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>; if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported; const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
return ( return (
<> <>
@@ -208,7 +207,7 @@ export function BillDetailEditcontainer({
initialValues={transformData(data)} initialValues={transformData(data)}
layout="vertical" layout="vertical"
> >
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse}/> <BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider> <Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? ( {bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery <JobsDocumentsLocalGallery

View File

@@ -47,7 +47,6 @@ export function BillFormComponent({
loadLines, loadLines,
billEdit, billEdit,
disableInvNumber, disableInvNumber,
disableInHouse,
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
@@ -199,7 +198,7 @@ export function BillFormComponent({
]} ]}
> >
<VendorSearchSelect <VendorSearchSelect
disabled={disabled || disableInHouse} disabled={disabled}
options={vendorAutoCompleteOptions} options={vendorAutoCompleteOptions}
preferredMake={preferredMake} preferredMake={preferredMake}
onSelect={handleVendorSelect} onSelect={handleVendorSelect}
@@ -272,7 +271,7 @@ export function BillFormComponent({
}), }),
]} ]}
> >
<Input disabled={disabled || disableInvNumber || disableInHouse} /> <Input disabled={disabled || disableInvNumber} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("bills.fields.date")} label={t("bills.fields.date")}

View File

@@ -22,7 +22,6 @@ export function BillFormContainer({
billEdit, billEdit,
disabled, disabled,
disableInvNumber, disableInvNumber,
disableInHouse
}) { }) {
const { Simple_Inventory } = useTreatments( const { Simple_Inventory } = useTreatments(
["Simple_Inventory"], ["Simple_Inventory"],
@@ -58,7 +57,6 @@ export function BillFormContainer({
job={lineData ? lineData.jobs_by_pk : null} job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null} responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber} disableInvNumber={disableInvNumber}
disableInHouse={disableInHouse}
loadOutstandingReturns={loadOutstandingReturns} loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory} loadInventory={loadInventory}
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null} preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}

View File

@@ -13,6 +13,7 @@ import {
notification, notification,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -21,6 +22,7 @@ import {
INSERT_PAYMENT_RESPONSE, INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PKS, QUERY_RO_AND_OWNER_BY_JOB_PKS,
} from "../../graphql/payment_response.queries"; } from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors"; import { selectCardPayment } from "../../redux/modals/modals.selectors";
@@ -46,12 +48,12 @@ const CardPaymentModalComponent = ({
toggleModalVisible, toggleModalVisible,
insertAuditTrail, insertAuditTrail,
}) => { }) => {
const { context, actions } = cardPaymentModal; const { context } = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,6 +65,7 @@ const CardPaymentModalComponent = ({
} }
); );
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window. //Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => { const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions."); console.log("*** Set IntelliPay callback functions.");
@@ -71,20 +74,16 @@ const CardPaymentModalComponent = ({
}); });
window.intellipay.runOnApproval(async function (response) { window.intellipay.runOnApproval(async function (response) {
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. console.warn("*** Running On Approval Script ***");
//Add a slight delay to allow the refetch to properly get the data. form.setFieldValue("paymentResponse", response);
setTimeout(() => { form.submit();
if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
}); });
window.intellipay.runOnNonApproval(async function (response) { window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment // Mutate unsuccessful payment
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
await insertPaymentResponse({ await insertPaymentResponse({
variables: { variables: {
paymentResponse: payments.map((payment) => ({ paymentResponse: payments.map((payment) => ({
@@ -109,9 +108,50 @@ const CardPaymentModalComponent = ({
}); });
}; };
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 () => { const handleIntelliPayCharge = async () => {
setLoading(true); setLoading(true);
//Validate //Validate
try { try {
await form.validateFields(); await form.validateFields();
@@ -124,7 +164,6 @@ const CardPaymentModalComponent = ({
const response = await axios.post("/intellipay/lightbox_credentials", { const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop, bodyshop,
refresh: !!window.intellipay, refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
}); });
if (window.intellipay) { if (window.intellipay) {
@@ -153,6 +192,7 @@ const CardPaymentModalComponent = ({
<Card title="Card Payment"> <Card title="Card Payment">
<Spin spinning={loading}> <Spin spinning={loading}>
<Form <Form
onFinish={handleFinish}
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
@@ -233,14 +273,23 @@ const CardPaymentModalComponent = ({
} }
> >
{() => { {() => {
console.log("Updating the owner info section.");
//If all of the job ids have been fileld in, then query and update the IP field. //If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
if ( if (
payments?.length > 0 && payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length payments?.filter((p) => p?.jobid).length === payments?.length
) { ) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) }); 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 ( return (
<> <>
<Input <Input
@@ -295,13 +344,6 @@ const CardPaymentModalComponent = ({
value={totalAmountToCharge?.toFixed(2)} value={totalAmountToCharge?.toFixed(2)}
hidden hidden
/> />
<Input
className="ipayfield"
data-ipayname="comment"
//type="hidden"
value={btoa(JSON.stringify(payments))}
hidden
/>
<Button <Button
type="primary" type="primary"
// data-ipayname="submit" // data-ipayname="submit"
@@ -316,6 +358,11 @@ const CardPaymentModalComponent = ({
); );
}} }}
</Form.Item> </Form.Item>
{/* Lightbox payment response when it is completed */}
<Form.Item name="paymentResponse" hidden>
<Input type="hidden" />
</Form.Item>
</Form> </Form>
</Spin> </Spin>
</Card> </Card>

View File

@@ -37,9 +37,6 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.leasereturn"> <Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")} {t("courtesycars.status.leasereturn")}
</Option> </Option>
<Option value="courtesycars.status.unavailable">
{t("courtesycars.status.unavailable")}
</Option>
</Select> </Select>
); );
}; };

View File

@@ -77,10 +77,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.leasereturn"), text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn", value: "courtesycars.status.leasereturn",
}, },
{
text: t("courtesycars.status.unavailable"),
value: "courtesycars.status.unavailable",
},
], ],
onFilter: (value, record) => record.status === value, onFilter: (value, record) => record.status === value,
sortOrder: sortOrder:

View File

@@ -3,13 +3,16 @@ import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() { export default function GlobalSearchOs() {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState(false); const [data, setData] = useState(false);
@@ -18,7 +21,7 @@ export default function GlobalSearchOs() {
try { try {
setLoading(true); setLoading(true);
const searchData = await axios.post("/search", { const searchData = await axios.post("/search", {
search: v search: v,
}); });
const resultsByType = { const resultsByType = {
@@ -26,7 +29,7 @@ export default function GlobalSearchOs() {
jobs: [], jobs: [],
bills: [], bills: [],
owners: [], owners: [],
vehicles: [] vehicles: [],
}; };
searchData.data.hits.hits.forEach((hit) => { searchData.data.hits.hits.forEach((hit) => {
@@ -47,14 +50,16 @@ export default function GlobalSearchOs() {
<span> <span>
<OwnerNameDisplay ownerObject={job} /> <OwnerNameDisplay ownerObject={job} />
</span> </span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span> <span>{`${job.v_model_yr || ""} ${
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span> <span>{`${job.clm_no || ""}`}</span>
<span>{`${job.plate_no || ""}`}</span> <span>{`${job.plate_no || ""}`}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.owners")), label: renderTitle(t("menus.header.search.owners")),
@@ -64,39 +69,53 @@ export default function GlobalSearchOs() {
value: OwnerNameDisplayFunction(owner), value: OwnerNameDisplayFunction(owner),
label: ( label: (
<Link to={`/manage/owners/${owner.id}`}> <Link to={`/manage/owners/${owner.id}`}>
<Space size="small" split={<Divider type="vertical" />} wrap> <Space
size="small"
split={<Divider type="vertical" />}
wrap
>
<span> <span>
<OwnerNameDisplay ownerObject={owner} /> <OwnerNameDisplay ownerObject={owner} />
</span> </span>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter> <PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter> {owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.vehicles")), label: renderTitle(t("menus.header.search.vehicles")),
options: resultsByType.vehicles.map((vehicle) => { options: resultsByType.vehicles.map((vehicle) => {
return { return {
key: vehicle.id, key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: ( label: (
<Link to={`/manage/vehicles/${vehicle.id}`}> <Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span> <span>
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} {`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span> </span>
<span>{vehicle.plate_no || ""}</span> <span>{vehicle.plate_no || ""}</span>
<span> <span>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay> <VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span> </span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.payments")), label: renderTitle(t("menus.header.search.payments")),
@@ -114,9 +133,9 @@ export default function GlobalSearchOs() {
<span>{payment.transactionid || ""}</span> <span>{payment.transactionid || ""}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.bills")), label: renderTitle(t("menus.header.search.bills")),
@@ -132,10 +151,10 @@ export default function GlobalSearchOs() {
<span>{bill.date}</span> <span>{bill.date}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
} },
// { // {
// label: renderTitle(t("menus.header.search.phonebook")), // label: renderTitle(t("menus.header.search.phonebook")),
// options: resultsByType.search_phonebook.map((pb) => { // options: resultsByType.search_phonebook.map((pb) => {
@@ -177,7 +196,15 @@ export default function GlobalSearchOs() {
}; };
return ( return (
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}> <AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
onClear={() => setData([])}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -3,19 +3,28 @@ import { AutoComplete, Divider, Input, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries"; import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() { export default function GlobalSearch() {
const { t } = useTranslation(); const { t } = useTranslation();
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY); const history = useHistory();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "" && v.variables.search.length >= 3) callSearch(v); if (
v &&
v.variables.search &&
v.variables.search !== "" &&
v.variables.search.length >= 3
)
callSearch(v);
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
@@ -44,13 +53,15 @@ export default function GlobalSearch() {
<span> <span>
<OwnerNameDisplay ownerObject={job} /> <OwnerNameDisplay ownerObject={job} />
</span> </span>
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`}</span> <span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</span>
<span>{`${job.clm_no || ""}`}</span> <span>{`${job.clm_no || ""}`}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.owners")), label: renderTitle(t("menus.header.search.owners")),
@@ -64,35 +75,45 @@ export default function GlobalSearch() {
<span> <span>
<OwnerNameDisplay ownerObject={owner} /> <OwnerNameDisplay ownerObject={owner} />
</span> </span>
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter> <PhoneNumberFormatter>
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter> {owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.vehicles")), label: renderTitle(t("menus.header.search.vehicles")),
options: data.search_vehicles.map((vehicle) => { options: data.search_vehicles.map((vehicle) => {
return { return {
key: vehicle.id, key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`, value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: ( label: (
<Link to={`/manage/vehicles/${vehicle.id}`}> <Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span> <span>
{`${vehicle.v_model_yr || ""} ${vehicle.v_make_desc || ""} ${vehicle.v_model_desc || ""}`} {`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span> </span>
<span>{vehicle.plate_no || ""}</span> <span>{vehicle.plate_no || ""}</span>
<span> <span>
<VehicleVinDisplay>{vehicle.v_vin || ""}</VehicleVinDisplay> <VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span> </span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.payments")), label: renderTitle(t("menus.header.search.payments")),
@@ -110,9 +131,9 @@ export default function GlobalSearch() {
<span>{payment.transactionid || ""}</span> <span>{payment.transactionid || ""}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.bills")), label: renderTitle(t("menus.header.search.bills")),
@@ -128,35 +149,46 @@ export default function GlobalSearch() {
<span>{bill.date}</span> <span>{bill.date}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
}, },
{ {
label: renderTitle(t("menus.header.search.phonebook")), label: renderTitle(t("menus.header.search.phonebook")),
options: data.search_phonebook.map((pb) => { options: data.search_phonebook.map((pb) => {
return { return {
key: pb.id, key: pb.id,
value: `${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`, value: `${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`,
label: ( label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}> <Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${pb.company || ""}`}</span> <span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
<PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter> <PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
<span>{pb.email}</span> <span>{pb.email}</span>
</Space> </Space>
</Link> </Link>
) ),
}; };
}) }),
} },
] ]
: []; : [];
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption> <AutoComplete
options={options}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
>
<Input.Search <Input.Search
size="large" size="large"
placeholder={t("general.labels.globalsearch")} placeholder={t("general.labels.globalsearch")}

View File

@@ -11,8 +11,9 @@ import Icon, {
FileAddFilled, FileAddFilled,
FileAddOutlined, FileAddOutlined,
FileFilled, FileFilled,
//GlobalOutlined,
HomeFilled, HomeFilled,
ImportOutlined, ImportOutlined, InfoCircleOutlined,
LineChartOutlined, LineChartOutlined,
PaperClipOutlined, PaperClipOutlined,
PhoneOutlined, PhoneOutlined,
@@ -22,39 +23,56 @@ import Icon, {
TeamOutlined, TeamOutlined,
ToolFilled, ToolFilled,
UnorderedListOutlined, UnorderedListOutlined,
UserOutlined UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd"; import {Layout, Menu, Switch, Tooltip} from "antd";
import React from "react"; import React, {useEffect, useState} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs"; import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa"; import {
FaCalendarAlt,
FaCarCrash,
FaCreditCard,
FaFileInvoiceDollar,
} from "react-icons/fa";
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
import { IoBusinessOutline } from "react-icons/io5"; import { IoBusinessOutline } from "react-icons/io5";
import { RiSurveyLine } from "react-icons/ri"; import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import {
selectRecentItems,
selectSelectedHeader,
} from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions"; import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
recentItems: selectRecentItems, recentItems: selectRecentItems,
selectedHeader: selectSelectedHeader, selectedHeader: selectSelectedHeader,
bodyshop: selectBodyshop bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), setBillEnterContext: (context) =>
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })), setTimeTicketContext: (context) =>
setReportCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "reportCenter" })), dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
setReportCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "reportCenter" })),
signOutStart: () => dispatch(signOutStart()), signOutStart: () => dispatch(signOutStart()),
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })) setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
}); });
function Header({ function Header({
@@ -68,26 +86,37 @@ function Header({
setPaymentContext, setPaymentContext,
setReportCenterContext, setReportCenterContext,
recentItems, recentItems,
setCardPaymentContext setCardPaymentContext,
}) { }) {
const { Simple_Inventory } = useTreatments(["Simple_Inventory"], {}, bodyshop && bodyshop.imexshopid); const { Simple_Inventory } = useTreatments(
const { DmsAp } = useTreatments(["DmsAp"], {}, bodyshop && bodyshop.imexshopid); ["Simple_Inventory"],
const { ImEXPay } = useTreatments(["ImEXPay"], {}, bodyshop && bodyshop.imexshopid); {},
bodyshop && bodyshop.imexshopid
);
const { DmsAp } = useTreatments(
["DmsAp"],
{},
bodyshop && bodyshop.imexshopid
);
const { ImEXPay } = useTreatments(
["ImEXPay"],
{},
bodyshop && bodyshop.imexshopid
);
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const deleteBetaCookie = () => { useEffect(() => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`)); const isBeta = checkBeta();
if (cookieExists) { setBetaSwitch(isBeta);
const domain = window.location.hostname.split(".").slice(-2).join("."); }, []);
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
deleteBetaCookie(); const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
}
return ( return (
<Layout.Header> <Layout.Header>
@@ -105,7 +134,11 @@ function Header({
<Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}> <Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}>
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link> <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu key="jobssubmenu" icon={<Icon component={FaCarCrash} />} title={t("menus.header.jobs")}> <Menu.SubMenu
key="jobssubmenu"
icon={<Icon component={FaCarCrash} />}
title={t("menus.header.jobs")}
>
<Menu.Item key="activejobs" icon={<FileFilled />}> <Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link> <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item> </Menu.Item>
@@ -116,7 +149,9 @@ function Header({
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link> <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="availablejobs" icon={<ImportOutlined />}> <Menu.Item key="availablejobs" icon={<ImportOutlined />}>
<Link to="/manage/available">{t("menus.header.availablejobs")}</Link> <Link to="/manage/available">
{t("menus.header.availablejobs")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="newjob" icon={<FileAddOutlined />}> <Menu.Item key="newjob" icon={<FileAddOutlined />}>
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link> <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
@@ -127,17 +162,25 @@ function Header({
</Menu.Item> </Menu.Item>
<Menu.Divider key="div2" /> <Menu.Divider key="div2" />
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}> <Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
<Link to="/manage/production/list">{t("menus.header.productionlist")}</Link> <Link to="/manage/production/list">
{t("menus.header.productionlist")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}> <Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}>
<Link to="/manage/production/board">{t("menus.header.productionboard")}</Link> <Link to="/manage/production/board">
{t("menus.header.productionboard")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Divider key="div3" /> <Menu.Divider key="div3" />
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}> <Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link> <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="customers" icon={<UserOutlined />} title={t("menus.header.customers")}> <Menu.SubMenu
key="customers"
icon={<UserOutlined />}
title={t("menus.header.customers")}
>
<Menu.Item key="owners" icon={<TeamOutlined />}> <Menu.Item key="owners" icon={<TeamOutlined />}>
<Link to="/manage/owners">{t("menus.header.owners")}</Link> <Link to="/manage/owners">{t("menus.header.owners")}</Link>
</Menu.Item> </Menu.Item>
@@ -145,19 +188,36 @@ function Header({
<Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link> <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="ccs" icon={<CarFilled />} title={t("menus.header.courtesycars")}> <Menu.SubMenu
key="ccs"
icon={<CarFilled />}
title={t("menus.header.courtesycars")}
>
<Menu.Item key="courtesycarsall" icon={<CarFilled />}> <Menu.Item key="courtesycarsall" icon={<CarFilled />}>
<Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link> <Link to="/manage/courtesycars">
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="contracts" icon={<FileFilled />}> <Menu.Item key="contracts" icon={<FileFilled />}>
<Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link> <Link to="/manage/courtesycars/contracts">
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="newcontract" icon={<FileAddFilled />}> <Menu.Item key="newcontract" icon={<FileAddFilled />}>
<Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link> <Link to="/manage/courtesycars/contracts/new">
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="accounting" icon={<DollarCircleFilled />} title={t("menus.header.accounting")}> <Menu.SubMenu
<Menu.Item key="bills" icon={<Icon component={FaFileInvoiceDollar} />}> key="accounting"
icon={<DollarCircleFilled />}
title={t("menus.header.accounting")}
>
<Menu.Item
key="bills"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/bills">{t("menus.header.bills")}</Link> <Link to="/manage/bills">{t("menus.header.bills")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
@@ -166,7 +226,7 @@ function Header({
onClick={() => { onClick={() => {
setBillEnterContext({ setBillEnterContext({
actions: {}, actions: {},
context: {} context: {},
}); });
}} }}
> >
@@ -175,8 +235,13 @@ function Header({
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<> <>
<Menu.Divider key="div4" /> <Menu.Divider key="div4" />
<Menu.Item key="inventory" icon={<Icon component={FaFileInvoiceDollar} />}> <Menu.Item
<Link to="/manage/inventory">{t("menus.header.inventory")}</Link> key="inventory"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/inventory">
{t("menus.header.inventory")}
</Link>
</Menu.Item> </Menu.Item>
</> </>
)} )}
@@ -189,7 +254,7 @@ function Header({
onClick={() => { onClick={() => {
setPaymentContext({ setPaymentContext({
actions: {}, actions: {},
context: null context: null,
}); });
}} }}
icon={<Icon component={FaCreditCard} />} icon={<Icon component={FaCreditCard} />}
@@ -202,7 +267,7 @@ function Header({
onClick={() => { onClick={() => {
setCardPaymentContext({ setCardPaymentContext({
actions: {}, actions: {},
context: {} context: {},
}); });
}} }}
icon={<Icon component={FaCreditCard} />} icon={<Icon component={FaCreditCard} />}
@@ -212,7 +277,9 @@ function Header({
)} )}
<Menu.Divider key="div5" /> <Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}> <Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link> <Link to="/manage/timetickets">
{t("menus.header.timetickets")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key="entertimetickets" key="entertimetickets"
@@ -223,31 +290,49 @@ function Header({
context: { context: {
created_by: currentUser.displayName created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName) ? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email : currentUser.email,
} },
}); });
}} }}
> >
{t("menus.header.entertimeticket")} {t("menus.header.entertimeticket")}
</Menu.Item> </Menu.Item>
<Menu.Divider key="div6" /> <Menu.Divider key="div6" />
<Menu.SubMenu key="accountingexport" title={t("menus.header.export")} icon={<ExportOutlined />}> <Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")}
icon={<ExportOutlined />}
>
<Menu.Item key="receivables"> <Menu.Item key="receivables">
<Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link> <Link to="/manage/accounting/receivables">
{t("menus.header.accounting-receivables")}
</Link>
</Menu.Item> </Menu.Item>
{(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || {(!(
(bodyshop && bodyshop.cdk_dealerid) ||
(bodyshop && bodyshop.pbs_serialnumber)
) ||
DmsAp.treatment === "on") && ( DmsAp.treatment === "on") && (
<Menu.Item key="payables"> <Menu.Item key="payables">
<Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link> <Link to="/manage/accounting/payables">
{t("menus.header.accounting-payables")}
</Link>
</Menu.Item> </Menu.Item>
)} )}
{!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) && ( {!(
(bodyshop && bodyshop.cdk_dealerid) ||
(bodyshop && bodyshop.pbs_serialnumber)
) && (
<Menu.Item key="payments"> <Menu.Item key="payments">
<Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link> <Link to="/manage/accounting/payments">
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item key="export-logs"> <Menu.Item key="export-logs">
<Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link> <Link to="/manage/accounting/exportlogs">
{t("menus.header.export-logs")}
</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
</Menu.SubMenu> </Menu.SubMenu>
@@ -255,11 +340,19 @@ function Header({
<Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link> <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}> <Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}>
<Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link> <Link to="/manage/temporarydocs">
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.SubMenu key="shopsubmenu" title={t("menus.header.shop")} icon={<SettingOutlined />}> <Menu.SubMenu
key="shopsubmenu"
title={t("menus.header.shop")}
icon={<SettingOutlined />}
>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}> <Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link> <Link to="/manage/shop?tab=info">
{t("menus.header.shop_config")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}> <Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link> <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
@@ -270,20 +363,32 @@ function Header({
onClick={() => { onClick={() => {
setReportCenterContext({ setReportCenterContext({
actions: {}, actions: {},
context: {} context: {},
}); });
}} }}
> >
{t("menus.header.reportcenter")} {t("menus.header.reportcenter")}
</Menu.Item> </Menu.Item>
<Menu.Item key="shop-vendors" icon={<Icon component={IoBusinessOutline} />}> <Menu.Item
<Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link> key="shop-vendors"
icon={<Icon component={IoBusinessOutline} />}
>
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}> <Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}>
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link> <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
<Menu.SubMenu key="user" title={currentUser.displayName || currentUser.email || t("general.labels.unknown")}> <Menu.SubMenu
key="user"
title={
currentUser.displayName ||
currentUser.email ||
t("general.labels.unknown")
}
>
<Menu.Item key="signout" danger onClick={() => signOutStart()}> <Menu.Item key="signout" danger onClick={() => signOutStart()}>
{t("user.actions.signout")} {t("user.actions.signout")}
</Menu.Item> </Menu.Item>
@@ -339,6 +444,17 @@ function Header({
</Menu.Item> </Menu.Item>
))} ))}
</Menu.SubMenu> </Menu.SubMenu>
<Menu.Item style={{marginLeft: 'auto'}} key="profile">
<Tooltip title="A more modern ImEX Online is ready for you to try! You can switch back at any time.">
<InfoCircleOutlined/>
<span style={{marginRight: 8}}>Try the new ImEX Online</span>
<Switch
checked={betaSwitch}
onChange={betaSwitchChange}
/>
</Tooltip>
</Menu.Item>
</Menu> </Menu>
</Layout.Header> </Layout.Header>
); );

View File

@@ -2,25 +2,13 @@ import { useQuery } from "@apollo/client";
import { Col, Row, Skeleton, Timeline, Typography } from "antd"; import { Col, Row, Skeleton, Timeline, Typography } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries"; import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
const mapStateToProps = createStructuredSelector({ export default function JobLinesExpander({ jobline, jobid }) {
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, technician }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, { const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -45,15 +33,11 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
<Timeline.Item key={line.id}> <Timeline.Item key={line.id}>
<Row wrap> <Row wrap>
<Col span={4}> <Col span={4}>
{!technician ? ( <Link
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}
to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`} >
> {line.parts_order.order_number}
{line.parts_order.order_number} </Link>
</Link>
) : (
`${line.parts_order.order_number}`
)}
</Col> </Col>
<Col span={4}> <Col span={4}>
<DateFormatter>{line.parts_order.order_date}</DateFormatter> <DateFormatter>{line.parts_order.order_date}</DateFormatter>
@@ -79,22 +63,17 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
</Col> </Col>
<Col md={24} lg={12}> <Col md={24} lg={12}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title> <Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<BillDetailEditcontainer />
<Timeline> <Timeline>
{data.billlines.length > 0 ? ( {data.billlines.length > 0 ? (
data.billlines.map((line) => ( data.billlines.map((line) => (
<Timeline.Item key={line.id}> <Timeline.Item key={line.id}>
<Row wrap> <Row wrap>
<Col span={4}> <Col span={4}>
{!technician ? ( <Link
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}
to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`} >
> {line.bill.invoice_number}
{line.bill.invoice_number} </Link>
</Link>
) : (
`${line.bill.invoice_number}`
)}
</Col> </Col>
<Col span={4}> <Col span={4}>
<span> <span>
@@ -116,7 +95,9 @@ export function JobLinesExpander({ jobline, jobid, technician }) {
</Timeline.Item> </Timeline.Item>
)) ))
) : ( ) : (
<Timeline.Item>{t("bills.labels.nobilllines")}</Timeline.Item> <Timeline.Item>
{t("bills.labels.nobilllines")}
</Timeline.Item>
)} )}
</Timeline> </Timeline>
</Col> </Col>

View File

@@ -1,12 +1,12 @@
import { import {
DeleteFilled, DeleteFilled,
EditFilled,
FilterFilled, FilterFilled,
HomeOutlined,
MinusCircleTwoTone,
PlusCircleTwoTone,
SyncOutlined, SyncOutlined,
WarningFilled, WarningFilled,
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { import {
@@ -20,8 +20,6 @@ import {
Tag, Tag,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -30,19 +28,23 @@ import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper"; import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import JobLineLocationPopup from "../job-line-location-popup/job-line-location-popup.component"; import JobLineLocationPopup from "../job-line-location-popup/job-line-location-popup.component";
import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component"; import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component";
import JobLineStatusPopup from "../job-line-status-popup/job-line-status-popup.component"; import JobLineStatusPopup from "../job-line-status-popup/job-line-status-popup.component";
import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-reference.component"; import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-reference.component";
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component"; // import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component"; 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";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -55,8 +57,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })), dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setPartsReceiveContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsReceive" })),
setBillEnterContext: (context) => setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
}); });
@@ -66,7 +66,6 @@ export function JobLinesComponent({
jobRO, jobRO,
technician, technician,
setPartsOrderContext, setPartsOrderContext,
setPartsReceiveContext,
loading, loading,
refetch, refetch,
jobLines, jobLines,
@@ -75,8 +74,6 @@ export function JobLinesComponent({
setJobLineEditContext, setJobLineEditContext,
form, form,
setBillEnterContext, setBillEnterContext,
billsQuery,
handlePartsOrderOnRowClick,
}) { }) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK); const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -344,7 +341,7 @@ export function JobLinesComponent({
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Space> <Space>
{(record.manual_line || jobIsPrivate) && !technician && ( {(record.manual_line || jobIsPrivate) && (
<> <>
<Button <Button
disabled={jobRO} disabled={jobRO}
@@ -427,14 +424,6 @@ export function JobLinesComponent({
return ( return (
<div> <div>
<PartsOrderModalContainer /> <PartsOrderModalContainer />
{!technician && (
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handlePartsOrderOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
/>
)}
<PageHeader <PageHeader
title={t("jobs.labels.estimatelines")} title={t("jobs.labels.estimatelines")}
extra={ extra={

View File

@@ -1,15 +1,6 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component"; import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
job,
joblines,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
refetch,
form,
...rest
}) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const jobLines = useMemo(() => { const jobLines = useMemo(() => {
@@ -46,9 +37,6 @@ function JobLinesContainer({
<JobLinesComponent <JobLinesComponent
refetch={refetch} refetch={refetch}
jobLines={jobLines} jobLines={jobLines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
setSearchText={setSearchText} setSearchText={setSearchText}
job={job} job={job}
form={form} form={form}

View File

@@ -23,18 +23,14 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
@@ -45,7 +41,6 @@ export function JobLineConvertToLabor({
jobline, jobline,
job, job,
insertAuditTrail, insertAuditTrail,
technician,
...otherBtnProps ...otherBtnProps
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -227,7 +222,7 @@ export function JobLineConvertToLabor({
return ( return (
<> <>
{children} {children}
{jobline.act_price !== 0 && !technician && ( {jobline.act_price !== 0 && (
<Popover <Popover
disabled={jobline.convertedtolbr} disabled={jobline.convertedtolbr}
content={overlay} content={overlay}

View File

@@ -304,7 +304,7 @@ export function JobsDetailHeaderActions({
disabled={!job.converted} disabled={!job.converted}
onClick={() => { onClick={() => {
setCardPaymentContext({ setCardPaymentContext({
actions: { refetch }, actions: {},
context: { jobid: job.id }, context: { jobid: job.id },
}); });
}} }}

View File

@@ -1,12 +1,44 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react"; import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
import JobsDetailPliComponent from "./jobs-detail-pli.component"; import JobsDetailPliComponent from "./jobs-detail-pli.component";
export default function JobsDetailPliContainer({ export default function JobsDetailPliContainer({ job }) {
job, const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
billsQuery, variables: { jobid: job.id },
handleBillOnRowClick, fetchPolicy: "network-only",
handlePartsOrderOnRowClick, nextFetchPolicy: "network-only",
}) { });
const search = queryString.parse(useLocation().search);
const history = useHistory();
const handleBillOnRowClick = (record) => {
if (record) {
if (record.id) {
search.billid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.billid;
history.push({ search: queryString.stringify(search) });
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history.push({ search: queryString.stringify(search) });
}
};
return ( return (
<JobsDetailPliComponent <JobsDetailPliComponent
job={job} job={job}

View File

@@ -11,7 +11,6 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
import StartChatButton from "../chat-open-button/chat-open-button.component"; import StartChatButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
@@ -37,10 +36,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: search?.search sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
? (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
: true,
sortOrder: sortcolumn === "ro_number" && sortorder, sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}> <Link to={"/manage/jobs/" + record.id}>
@@ -54,6 +50,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
key: "ownr_ln", key: "ownr_ln",
ellipsis: true, ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder, //sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
@@ -71,6 +68,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph1"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph1",
key: "ownr_ph1", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
render: (text, record) => ( render: (text, record) => (
<StartChatButton phone={record.ownr_ph1} jobid={record.id} /> <StartChatButton phone={record.ownr_ph1} jobid={record.id} />
@@ -80,6 +78,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ownr_ph2"), title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2", dataIndex: "ownr_ph2",
key: "ownr_ph2", key: "ownr_ph2",
ellipsis: true, ellipsis: true,
render: (text, record) => ( render: (text, record) => (
<StartChatButton phone={record.ownr_ph2} jobid={record.id} /> <StartChatButton phone={record.ownr_ph2} jobid={record.id} />
@@ -89,8 +88,9 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true, sorter: true, // (a, b) => alphaSort(a.status, b.status),
sortOrder: sortcolumn === "status" && sortorder, sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.status || t("general.labels.na"); return record.status || t("general.labels.na");
@@ -106,6 +106,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
@@ -126,7 +127,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "plate_no", dataIndex: "plate_no",
key: "plate_no", key: "plate_no",
ellipsis: true, ellipsis: true,
sorter: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true, sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: sortcolumn === "plate_no" && sortorder, sortOrder: sortcolumn === "plate_no" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.plate_no ? record.plate_no : ""; return record.plate_no ? record.plate_no : "";
@@ -137,7 +138,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "clm_no", dataIndex: "clm_no",
key: "clm_no", key: "clm_no",
ellipsis: true, ellipsis: true,
sorter: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true, sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder, sortOrder: sortcolumn === "clm_no" && sortorder,
render: (text, record) => render: (text, record) =>
`${record.clm_no || ""}${ `${record.clm_no || ""}${
@@ -155,7 +156,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
dataIndex: "clm_total", dataIndex: "clm_total",
key: "clm_total", key: "clm_total",
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true, sorter: true, //(a, b) => a.clm_total - b.clm_total,
sortOrder: sortcolumn === "clm_total" && sortorder, sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.clm_total ? ( return record.clm_total ? (
@@ -169,6 +170,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.owner_owing"), title: t("jobs.fields.owner_owing"),
dataIndex: "owner_owing", dataIndex: "owner_owing",
key: "owner_owing", key: "owner_owing",
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter> <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
), ),

View File

@@ -6,8 +6,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { alphaSort, statusSort } from "../../utils/sorters";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component"; import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -87,18 +86,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
})), })),
onFilter: (value, record) => value.includes(record.status), onFilter: (value, record) => value.includes(record.status),
}, },
{
title: t("jobs.fields.actual_completion"),
dataIndex: "actual_completion",
key: "actual_completion",
render: (text, record) => (
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
),
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
sortOrder:
state.sortedInfo.columnKey === "actual_completion" &&
state.sortedInfo.order,
},
{ {
title: t("jobs.fields.clm_total"), title: t("jobs.fields.clm_total"),
dataIndex: "clm_total", dataIndex: "clm_total",

View File

@@ -1,416 +0,0 @@
import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Drawer,
Grid,
PageHeader,
Popconfirm,
Space,
Table,
} from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
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 } from "../../utils/sorters";
import DataLabel from "../data-label/data-label.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "billEnter",
})
),
setPartsReceiveContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "partsReceive",
})
),
});
export function PartsOrderListTableDrawerComponent({
setBillEnterContext,
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setPartsReceiveContext,
}) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", { job });
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
const [billData, setBillData] = useState(null);
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const selectedPartsOrderRecord = parts_orders.find(
(r) => r.id === selectedpartsorder
);
useEffect(() => {
const fetchData = async () => {
if (selectedPartsOrderRecord?.returnfrombill) {
try {
const { data } = await billQuery({
variables: { billid: selectedPartsOrderRecord.returnfrombill },
});
setBillData(data);
} catch (error) {
console.error("Error fetching bill data:", error);
}
} else setBillData(null);
};
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
const recordActions = (record) => (
<Space direction="horizontal" wrap>
<Button
disabled={
jobRO ||
record.return ||
record.vendor.id === bodyshop.inhousevendorid
}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setPartsReceiveContext({
actions: { refetch: refetch },
context: {
jobId: job.id,
job: job,
partsorderlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
})),
},
});
}}
>
{t("parts_orders.actions.receive")}
</Button>
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrder({
variables: { partsOrderId: record.id },
update(cache) {
cache.modify({
fields: {
parts_orders(existingPartsOrders, { readField }) {
return existingPartsOrders.filter(
(billref) => record.id !== readField("id", billref)
);
},
},
});
},
});
}}
>
<Button disabled={jobRO}>
<DeleteFilled />
</Button>
</Popconfirm>
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
<Button
disabled={
(jobRO ? !record.return : jobRO) ||
record.vendor.id === bodyshop.inhousevendorid
}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setBillEnterContext({
actions: { refetch: refetch },
context: {
job: job,
bill: {
vendorid: record.vendor.id,
is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
pol.jobline.part_type
] ||
null)
: null,
})),
},
},
});
}}
>
{t("parts_orders.actions.receivebill")}
</Button>
</FeatureWrapperComponent>
<PrintWrapper
templateObject={{
name: record.return
? Templates.parts_return_slip.key
: Templates.parts_order.key,
variables: { id: record.id },
}}
messageObject={{
subject: record.return
? Templates.parts_return_slip.subject
: Templates.parts_order.subject,
to: record.vendor.email,
}}
id={job.id}
/>
</Space>
);
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
),
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cost"),
dataIndex: "cost",
key: "cost",
sorter: (a, b) => a.cost - b.cost,
sortOrder:
state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.cost}</CurrencyFormatter>
),
},
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
key: "backordered_on",
render: (text, record) => <DateFormatter>{text}</DateFormatter>,
},
{
title: t("parts_orders.fields.backordered_eta"),
dataIndex: "backordered_eta",
key: "backordered_eta",
render: (text, record) => (
<PartsOrderBackorderEta
backordered_eta={record.backordered_eta}
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
),
},
];
return (
<div>
<PageHeader
title={
billData
? `${record.vendor.name} - ${record.order_number} - ${t(
"bills.labels.returnfrombill"
)}: ${billData.bills_by_pk.invoice_number}`
: `${record.vendor.name} - ${record.order_number}`
}
extra={recordActions(record)}
/>
<Table
scroll={{
x: true, //y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
onChange={handleTableChange}
/>
<DataLabel label={t("parts_orders.fields.comments")}>
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel>
</div>
);
};
return (
<div>
<PartsReceiveModalContainer />
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
open={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsOrderListTableDrawerComponent);

View File

@@ -1,21 +1,39 @@
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons"; import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd"; import {
Button,
Card,
Checkbox,
Drawer,
Grid,
Input,
PageHeader,
Popconfirm,
Space,
Table,
} from "antd";
import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries"; import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -38,6 +56,21 @@ export function PartsOrderListTableComponent({
handleOnRowClick, handleOnRowClick,
setPartsReceiveContext, setPartsReceiveContext,
}) { }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers; const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", { job }); const Templates = TemplateList("partsorder", { job });
@@ -45,8 +78,10 @@ export function PartsOrderListTableComponent({
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
}); });
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
@@ -55,11 +90,7 @@ export function PartsOrderListTableComponent({
const recordActions = (record, showView = false) => ( const recordActions = (record, showView = false) => (
<Space wrap> <Space wrap>
{showView && ( {showView && (
<Button <Button onClick={() => handleOnRowClick(record)}>
onClick={() => {
handleOnRowClick(record);
}}
>
<EyeFilled /> <EyeFilled />
</Button> </Button>
)} )}
@@ -135,7 +166,7 @@ export function PartsOrderListTableComponent({
is_credit_memo: record.return, is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => { billlines: record.parts_order_lines.map((pol) => {
return { return {
joblineid: pol.job_line_id || "noline", joblineid: pol.job_line_id,
line_desc: pol.line_desc, line_desc: pol.line_desc,
quantity: pol.quantity, quantity: pol.quantity,
@@ -246,6 +277,164 @@ export function PartsOrderListTableComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const selectedPartsOrderRecord = parts_orders.find(
(r) => r.id === selectedpartsorder
);
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
),
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cost"),
dataIndex: "cost",
key: "cost",
sorter: (a, b) => a.cost - b.cost,
sortOrder:
state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.cost}</CurrencyFormatter>
),
},
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
),
},
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
key: "backordered_on",
render: (text, record) => <DateFormatter>{text}</DateFormatter>,
},
{
title: t("parts_orders.fields.backordered_eta"),
dataIndex: "backordered_eta",
key: "backordered_eta",
render: (text, record) => (
<PartsOrderBackorderEta
backordered_eta={record.backordered_eta}
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
),
},
];
return (
<div>
<PageHeader
title={record && `${record.vendor.name} - ${record.order_number}`}
extra={recordActions(record)}
/>
<Table
scroll={{
x: true, //y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
/>
<DataLabel label={t("parts_orders.fields.comments")}>
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel>
</div>
);
};
const filteredPartsOrders = parts_orders const filteredPartsOrders = parts_orders
? searchText === "" ? searchText === ""
? parts_orders ? parts_orders
@@ -281,12 +470,15 @@ export function PartsOrderListTableComponent({
} }
> >
<PartsReceiveModalContainer /> <PartsReceiveModalContainer />
<PartsOrderDrawer <Drawer
job={job} placement="right"
billsQuery={billsQuery} onClose={() => handleOnRowClick(null)}
handleOnRowClick={handleOnRowClick} visible={selectedpartsorder}
setPartsReceiveContext={setPartsReceiveContext} closable
/> width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
<Table <Table
loading={billsQuery.loading} loading={billsQuery.loading}
scroll={{ scroll={{

View File

@@ -139,8 +139,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
contentStyle={{ fontWeight: "600" }} contentStyle={{ fontWeight: "600" }}
column={4} column={4}
> >
<Descriptions.Item label={t("job_payments.titles.hint")}> <Descriptions.Item label={t("job_payments.titles.payer")}>
{payment_response?.response?.methodhint} {record.payer}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.payername")}> <Descriptions.Item label={t("job_payments.titles.payername")}>
{payment_response?.response?.nameOnCard ?? ""} {payment_response?.response?.nameOnCard ?? ""}
@@ -155,7 +155,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
{record.transactionid} {record.transactionid}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentid")}> <Descriptions.Item label={t("job_payments.titles.paymentid")}>
{payment_response?.ext_paymentid ?? ""} {payment_response?.response?.paymentreferenceid ?? ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymenttype")}> <Descriptions.Item label={t("job_payments.titles.paymenttype")}>
{record.type} {record.type}

View File

@@ -9,7 +9,9 @@ import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component"; import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component"; import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component"; import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component"; import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -32,7 +34,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
dataIndex: "viewdetail", dataIndex: "viewdetail",
key: "viewdetail", key: "viewdetail",
ellipsis: true, ellipsis: true,
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link> render: (text, record) => (
<Link to={{ search: `?selected=${record.id}` }}>
{i18n.t("general.labels.view")}
</Link>
),
}, },
{ {
title: i18n.t("jobs.fields.ro_number"), title: i18n.t("jobs.fields.ro_number"),
@@ -40,18 +46,23 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "ro_number", key: "ro_number",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
technician ? ( technician ? (
<Link to={`/tech/joblookup?selected=${record.id}`}> <Link to={`/tech/joblookup?selected=${record.id}`}>
{record.ro_number} {record.ro_number}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />} {record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Link> </Link>
) : ( ) : (
<Link to={`/manage/jobs/${record.id}`}> <Link to={`/manage/jobs/${record.id}`}>
<Space> <Space>
{record.ro_number} {record.ro_number}
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />} {record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && ( {record.iouparent && (
<Tooltip title={i18n.t("jobs.labels.iou")}> <Tooltip title={i18n.t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} /> <BranchesOutlined style={{ color: "orangered" }} />
@@ -59,7 +70,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
)} )}
</Space> </Space>
</Link> </Link>
) ),
}, },
{ {
title: i18n.t("jobs.fields.owner"), title: i18n.t("jobs.fields.owner"),
@@ -74,8 +85,10 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
</Link> </Link>
), ),
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
}, },
{ {
title: i18n.t("jobs.fields.vehicle"), title: i18n.t("jobs.fields.vehicle"),
@@ -84,10 +97,13 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`, `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
), ),
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
technician ? ( technician ? (
<>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ <>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
@@ -99,7 +115,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
record.v_color || "" record.v_color || ""
} ${record.plate_no || ""}`}</Link> } ${record.plate_no || ""}`}</Link>
) ),
}, },
{ {
title: i18n.t("jobs.fields.actual_in"), title: i18n.t("jobs.fields.actual_in"),
@@ -107,8 +123,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "actual_in", key: "actual_in",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.actual_in, b.actual_in), sorter: (a, b) => dateSort(a.actual_in, b.actual_in),
sortOrder: state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListDate record={record} field="actual_in" time /> state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="actual_in" time />
),
}, },
{ {
title: i18n.t("jobs.fields.actual_in") + " (HH:MM)", title: i18n.t("jobs.fields.actual_in") + " (HH:MM)",
@@ -116,16 +135,28 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "actual_in_time", key: "actual_in_time",
ellipsis: true, ellipsis: true,
render: (text, record) => <TimeFormatter>{record.actual_in}</TimeFormatter> render: (text, record) => (
<TimeFormatter>{record.actual_in}</TimeFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.scheduled_completion"), title: i18n.t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion", dataIndex: "scheduled_completion",
key: "scheduled_completion", key: "scheduled_completion",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_completion, b.scheduled_completion), sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, dateSort(a.scheduled_completion, b.scheduled_completion),
render: (text, record) => <ProductionListDate record={record} field="scheduled_completion" pastIndicator time /> sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate
record={record}
field="scheduled_completion"
pastIndicator
time
/>
),
}, },
{ {
title: i18n.t("jobs.fields.scheduled_completion") + " (HH:MM)", title: i18n.t("jobs.fields.scheduled_completion") + " (HH:MM)",
@@ -133,7 +164,9 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "scheduled_completion_time", key: "scheduled_completion_time",
ellipsis: true, ellipsis: true,
render: (text, record) => <TimeFormatter>{record.scheduled_completion}</TimeFormatter> render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.date_last_contacted"), title: i18n.t("jobs.fields.date_last_contacted"),
@@ -141,8 +174,10 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "date_last_contacted", key: "date_last_contacted",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.date_last_contacted, b.date_last_contacted), sorter: (a, b) => dateSort(a.date_last_contacted, b.date_last_contacted),
sortOrder: state.sortedInfo.columnKey === "date_last_contacted" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListLastContacted record={record} /> state.sortedInfo.columnKey === "date_last_contacted" &&
state.sortedInfo.order,
render: (text, record) => <ProductionListLastContacted record={record} />,
}, },
{ {
title: i18n.t("jobs.fields.date_next_contact"), title: i18n.t("jobs.fields.date_next_contact"),
@@ -150,8 +185,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "date_next_contact", key: "date_next_contact",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.date_next_contact, b.date_next_contact), sorter: (a, b) => dateSort(a.date_next_contact, b.date_next_contact),
sortOrder: state.sortedInfo.columnKey === "date_next_contact" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListDate record={record} field="date_next_contact" pastIndicator time /> state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate
record={record}
field="date_next_contact"
pastIndicator
time
/>
),
}, },
{ {
title: i18n.t("jobs.fields.scheduled_delivery"), title: i18n.t("jobs.fields.scheduled_delivery"),
@@ -159,8 +203,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "scheduled_delivery", key: "scheduled_delivery",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery), sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListDate record={record} field="scheduled_delivery" pastIndicator time /> state.sortedInfo.columnKey === "scheduled_delivery" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate
record={record}
field="scheduled_delivery"
pastIndicator
time
/>
),
}, },
{ {
title: i18n.t("jobs.fields.scheduled_delivery") + " (HH:MM)", title: i18n.t("jobs.fields.scheduled_delivery") + " (HH:MM)",
@@ -168,7 +221,9 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "scheduled_delivery_time", key: "scheduled_delivery_time",
ellipsis: true, ellipsis: true,
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter> render: (text, record) => (
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.ins_co_nm"), title: i18n.t("jobs.fields.ins_co_nm"),
@@ -176,7 +231,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
}, },
{ {
title: i18n.t("jobs.fields.clm_no"), title: i18n.t("jobs.fields.clm_no"),
@@ -184,7 +240,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "clm_no", key: "clm_no",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
}, },
{ {
title: i18n.t("jobs.fields.clm_total"), title: i18n.t("jobs.fields.clm_total"),
@@ -192,8 +249,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "clm_total", key: "clm_total",
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total, sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, sortOrder:
render: (text, record) => <CurrencyFormatter>{record.clm_total}</CurrencyFormatter> state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.owner_owing"), title: i18n.t("jobs.fields.owner_owing"),
@@ -201,36 +261,49 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "owner_owing", key: "owner_owing",
ellipsis: true, ellipsis: true,
sorter: (a, b) => a.owner_owing - b.owner_owing, sorter: (a, b) => a.owner_owing - b.owner_owing,
sortOrder: state.sortedInfo.columnKey === "owner_owing" && state.sortedInfo.order, sortOrder:
render: (text, record) => <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter> state.sortedInfo.columnKey === "owner_owing" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.ownr_ph1"), title: i18n.t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph1",
key: "ownr_ph1", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
render: (text, record) => <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter> render: (text, record) => (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.ownr_ph2"), title: i18n.t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2", dataIndex: "ownr_ph2",
key: "ownr_ph2", key: "ownr_ph2",
ellipsis: true, ellipsis: true,
render: (text, record) => <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter> render: (text, record) => (
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
),
}, },
{ {
title: i18n.t("jobs.fields.specialcoveragepolicy"), title: i18n.t("jobs.fields.specialcoveragepolicy"),
dataIndex: "special_coverage_policy", dataIndex: "special_coverage_policy",
key: "special_coverage_policy", key: "special_coverage_policy",
ellipsis: true, ellipsis: true,
sorter: (a, b) => Number(a.special_coverage_policy) - Number(b.special_coverage_policy), sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "special_coverage_policy" && state.sortedInfo.order, Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
sortOrder:
state.sortedInfo.columnKey === "special_coverage_policy" &&
state.sortedInfo.order,
filters: [ filters: [
{ text: "True", value: true }, { text: "True", value: true },
{ text: "False", value: false } { text: "False", value: false },
], ],
onFilter: (value, record) => value === record.special_coverage_policy, onFilter: (value, record) =>
render: (text, record) => <Checkbox checked={record.special_coverage_policy} /> value.includes(record.special_coverage_policy),
render: (text, record) => (
<Checkbox checked={record.special_coverage_policy} />
),
}, },
{ {
@@ -239,13 +312,15 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters: filters:
(bodyshop && (bodyshop &&
bodyshop.appt_alt_transport.map((s) => { bodyshop.appt_alt_transport.map((s) => {
return { return {
text: s, text: s,
value: [s] value: [s],
}; };
})) || })) ||
[], [],
@@ -255,7 +330,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
{record.alt_transport} {record.alt_transport}
<JobAltTransportChange job={record} /> <JobAltTransportChange job={record} />
</div> </div>
) ),
}, },
{ {
title: i18n.t("jobs.fields.status"), title: i18n.t("jobs.fields.status"),
@@ -263,8 +338,9 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: (a, b) => statusSort(a.status, b.status, activeStatuses), sorter: (a, b) => statusSort(a.status, b.status, activeStatuses),
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListColumnStatus record={record} /> state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnStatus record={record} />,
}, },
{ {
title: i18n.t("jobs.fields.category"), title: i18n.t("jobs.fields.category"),
@@ -277,30 +353,37 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
bodyshop.md_categories.map((s) => { bodyshop.md_categories.map((s) => {
return { return {
text: s, text: s,
value: [s] value: [s],
}; };
})) || })) ||
[], [],
onFilter: (value, record) => value.includes(record.category), onFilter: (value, record) => value.includes(record.category),
sorter: (a, b) => alphaSort(a.category, b.category), sorter: (a, b) => alphaSort(a.category, b.category),
sortOrder: state.sortedInfo.columnKey === "category" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListColumnCategory record={record} /> state.sortedInfo.columnKey === "category" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnCategory record={record} />
),
}, },
{ {
title: i18n.t("production.labels.bodyhours"), title: i18n.t("production.labels.bodyhours"),
dataIndex: "labhrs", dataIndex: "labhrs",
key: "labhrs", key: "labhrs",
sorter: (a, b) => a.labhrs.aggregate.sum.mod_lb_hrs - b.labhrs.aggregate.sum.mod_lb_hrs, sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "labhrs" && state.sortedInfo.order, a.labhrs.aggregate.sum.mod_lb_hrs - b.labhrs.aggregate.sum.mod_lb_hrs,
render: (text, record) => record.labhrs.aggregate.sum.mod_lb_hrs sortOrder:
state.sortedInfo.columnKey === "labhrs" && state.sortedInfo.order,
render: (text, record) => record.labhrs.aggregate.sum.mod_lb_hrs,
}, },
{ {
title: i18n.t("production.labels.refinishhours"), title: i18n.t("production.labels.refinishhours"),
dataIndex: "larhrs", dataIndex: "larhrs",
key: "larhrs", key: "larhrs",
sorter: (a, b) => a.larhrs.aggregate.sum.mod_lb_hrs - b.larhrs.aggregate.sum.mod_lb_hrs, sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "larhrs" && state.sortedInfo.order, a.larhrs.aggregate.sum.mod_lb_hrs - b.larhrs.aggregate.sum.mod_lb_hrs,
render: (text, record) => record.larhrs.aggregate.sum.mod_lb_hrs sortOrder:
state.sortedInfo.columnKey === "larhrs" && state.sortedInfo.order,
render: (text, record) => record.larhrs.aggregate.sum.mod_lb_hrs,
}, },
{ {
title: i18n.t("production.labels.totalhours"), title: i18n.t("production.labels.totalhours"),
@@ -310,36 +393,38 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
a.labhrs.aggregate.sum.mod_lb_hrs + a.labhrs.aggregate.sum.mod_lb_hrs +
a.larhrs.aggregate.sum.mod_lb_hrs - a.larhrs.aggregate.sum.mod_lb_hrs -
(b.labhrs.aggregate.sum.mod_lb_hrs + b.larhrs.aggregate.sum.mod_lb_hrs), (b.labhrs.aggregate.sum.mod_lb_hrs + b.larhrs.aggregate.sum.mod_lb_hrs),
sortOrder: state.sortedInfo.columnKey === "totalhours" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "totalhours" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
(record.labhrs.aggregate.sum.mod_lb_hrs + record.larhrs.aggregate.sum.mod_lb_hrs).toFixed(1) (
record.labhrs.aggregate.sum.mod_lb_hrs +
record.larhrs.aggregate.sum.mod_lb_hrs
).toFixed(1),
}, },
{ {
title: i18n.t("production.labels.alert"), title: i18n.t("production.labels.alert"),
dataIndex: "alert", dataIndex: "alert",
key: "alert", key: "alert",
sorter: (a, b) => Number(a.production_vars?.alert || false) - Number(b.production_vars?.alert || false), sorter: (a, b) =>
sortOrder: state.sortedInfo.columnKey === "alert" && state.sortedInfo.order, Number(a.production_vars?.alert || false) -
filters: [ Number(b.production_vars?.alert || false),
{ text: "True", value: true }, sortOrder:
{ text: "False", value: false } state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
], render: (text, record) => <ProductionListColumnAlert record={record} />,
onFilter: (value, record) => value === (record.production_vars?.alert || false),
render: (text, record) => <ProductionListColumnAlert record={record} />
}, },
{ {
title: i18n.t("production.labels.note"), title: i18n.t("production.labels.note"),
dataIndex: "note", dataIndex: "note",
key: "note", key: "note",
ellipsis: true, ellipsis: true,
render: (text, record) => <ProductionListColumnNote record={record} /> render: (text, record) => <ProductionListColumnNote record={record} />,
}, },
{ {
title: i18n.t("production.labels.comment"), title: i18n.t("production.labels.comment"),
dataIndex: "comment", dataIndex: "comment",
key: "comment", key: "comment",
ellipsis: true, ellipsis: true,
render: (text, record) => <ProductionListColumnComment record={record} /> render: (text, record) => <ProductionListColumnComment record={record} />,
}, },
{ {
title: i18n.t("production.labels.touchtime"), title: i18n.t("production.labels.touchtime"),
@@ -347,7 +432,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "tt", key: "tt",
render: (text, record) => { render: (text, record) => {
return <ProductionlistColumnTouchTime job={record} />; return <ProductionlistColumnTouchTime job={record} />;
} },
}, },
{ {
title: i18n.t("production.labels.bodypriority"), title: i18n.t("production.labels.bodypriority"),
@@ -357,8 +442,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
sorter: (a, b) => sorter: (a, b) =>
((a.production_vars && a.production_vars.bodypriority) || 11) - ((a.production_vars && a.production_vars.bodypriority) || 11) -
((b.production_vars && b.production_vars.bodypriority) || 11), ((b.production_vars && b.production_vars.bodypriority) || 11),
sortOrder: state.sortedInfo.columnKey === "bodypriority" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListColumnBodyPriority record={record} /> state.sortedInfo.columnKey === "bodypriority" && state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnBodyPriority record={record} />
),
}, },
{ {
title: i18n.t("production.labels.paintpriority"), title: i18n.t("production.labels.paintpriority"),
@@ -368,8 +456,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
sorter: (a, b) => sorter: (a, b) =>
((a.production_vars && a.production_vars.paintpriority) || 11) - ((a.production_vars && a.production_vars.paintpriority) || 11) -
((b.production_vars && b.production_vars.paintpriority) || 11), ((b.production_vars && b.production_vars.paintpriority) || 11),
sortOrder: state.sortedInfo.columnKey === "paintpriority" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListColumnPaintPriority record={record} /> state.sortedInfo.columnKey === "paintpriority" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnPaintPriority record={record} />
),
}, },
{ {
title: i18n.t("production.labels.detailpriority"), title: i18n.t("production.labels.detailpriority"),
@@ -379,74 +471,110 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
sorter: (a, b) => sorter: (a, b) =>
((a.production_vars && a.production_vars.detailpriority) || 11) - ((a.production_vars && a.production_vars.detailpriority) || 11) -
((b.production_vars && b.production_vars.detailpriority) || 11), ((b.production_vars && b.production_vars.detailpriority) || 11),
sortOrder: state.sortedInfo.columnKey === "detailpriority" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListColumnDetailPriority record={record} /> state.sortedInfo.columnKey === "detailpriority" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListColumnDetailPriority record={record} />
),
}, },
{ {
title: i18n.t("production.labels.sublets"), title: i18n.t("production.labels.sublets"),
dataIndex: "sublets", dataIndex: "sublets",
key: "sublets", key: "sublets",
render: (text, record) => <ProductionSubletsManageComponent subletJobLines={record.subletLines} /> render: (text, record) => (
<ProductionSubletsManageComponent subletJobLines={record.subletLines} />
),
}, },
{ {
title: i18n.t("jobs.fields.employee_body"), title: i18n.t("jobs.fields.employee_body"),
dataIndex: "employee_body", dataIndex: "employee_body",
key: "employee_body", key: "employee_body",
sortOrder: state.sortedInfo.columnKey === "employee_body" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "employee_body" &&
state.sortedInfo.order,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
bodyshop.employees?.find((e) => e.id === a.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 bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
), ),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_body" /> render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_body"
/>
),
}, },
{ {
title: i18n.t("jobs.fields.employee_prep"), title: i18n.t("jobs.fields.employee_prep"),
dataIndex: "employee_prep", dataIndex: "employee_prep",
key: "employee_prep", key: "employee_prep",
sortOrder: state.sortedInfo.columnKey === "employee_prep" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "employee_prep" &&
state.sortedInfo.order,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
bodyshop.employees?.find((e) => e.id === a.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 bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
), ),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_prep" /> render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_prep"
/>
),
}, },
{ {
title: i18n.t("jobs.fields.employee_csr"), title: i18n.t("jobs.fields.employee_csr"),
dataIndex: "employee_csr", dataIndex: "employee_csr",
key: "employee_csr", key: "employee_csr",
sortOrder: state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
bodyshop.employees?.find((e) => e.id === a.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 bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
), ),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_csr" /> render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr" />
),
}, },
{ {
title: i18n.t("jobs.fields.employee_refinish"), title: i18n.t("jobs.fields.employee_refinish"),
dataIndex: "employee_refinish", dataIndex: "employee_refinish",
key: "employee_refinish", key: "employee_refinish",
sortOrder: state.sortedInfo.columnKey === "employee_refinish" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "employee_refinish" &&
state.sortedInfo.order,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
bodyshop.employees?.find((e) => e.id === a.employee_refinish)?.first_name, bodyshop.employees?.find((e) => e.id === a.employee_refinish)
bodyshop.employees?.find((e) => e.id === b.employee_refinish)?.first_name ?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)
?.first_name
), ),
render: (text, record) => <ProductionListEmployeeAssignment record={record} type="employee_refinish" /> render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
type="employee_refinish"
/>
),
}, },
{ {
title: i18n.t("jobs.labels.parts_received"), title: i18n.t("jobs.labels.parts_received"),
dataIndex: "parts_received", dataIndex: "parts_received",
key: "parts_received", key: "parts_received",
render: (text, record) => <ProductionListColumnPartsReceived record={record} /> render: (text, record) => (
<ProductionListColumnPartsReceived record={record} />
),
}, },
{ {
title: i18n.t("jobs.fields.partsstatus"), title: i18n.t("jobs.fields.partsstatus"),
dataIndex: "partsstatus", dataIndex: "partsstatus",
key: "partsstatus", key: "partsstatus",
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} record={record} /> render: (text, record) => (
<JobPartsQueueCount parts={record.joblines_status} record={record} />
),
}, },
{ {
title: i18n.t("jobs.labels.estimator"), title: i18n.t("jobs.labels.estimator"),
@@ -457,7 +585,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(), `${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim() `${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
), ),
sortOrder: state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order, sortOrder:
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filters: filters:
(data && (data &&
data data
@@ -466,12 +595,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
.map((s) => { .map((s) => {
return { return {
text: s || "N/A", text: s || "N/A",
value: [s] value: [s],
}; };
})) || })) ||
[], [],
onFilter: (value, record) => value.includes(`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()), onFilter: (value, record) =>
render: (text, record) => `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
}, },
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client. //Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
@@ -501,8 +634,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "date_repairstarted", key: "date_repairstarted",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.date_repairstarted, b.date_repairstarted), sorter: (a, b) => dateSort(a.date_repairstarted, b.date_repairstarted),
sortOrder: state.sortedInfo.columnKey === "date_repairstarted" && state.sortedInfo.order, sortOrder:
render: (text, record) => <ProductionListDate record={record} field="date_repairstarted" time /> state.sortedInfo.columnKey === "date_repairstarted" &&
state.sortedInfo.order,
render: (text, record) => (
<ProductionListDate record={record} field="date_repairstarted" time />
),
}, },
{ {
title: i18n.t("jobs.fields.date_repairstarted") + " (HH:MM)", title: i18n.t("jobs.fields.date_repairstarted") + " (HH:MM)",
@@ -510,8 +647,10 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
key: "date_repairstarted_time", key: "date_repairstarted_time",
ellipsis: true, ellipsis: true,
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter> render: (text, record) => (
} <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
),
},
]; ];
}; };
export default r; export default r;

View File

@@ -6,8 +6,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { alphaSort, statusSort } from "../../utils/sorters";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay, { import OwnerNameDisplay, {
OwnerNameDisplayFunction, OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component"; } from "../owner-name-display/owner-name-display.component";
@@ -80,18 +79,7 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
})), })),
onFilter: (value, record) => value.includes(record.status), onFilter: (value, record) => value.includes(record.status),
}, },
{
title: t("jobs.fields.actual_completion"),
dataIndex: "actual_completion",
key: "actual_completion",
render: (text, record) => (
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
),
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
sortOrder:
state.sortedInfo.columnKey === "actual_completion" &&
state.sortedInfo.order,
},
{ {
title: t("jobs.fields.clm_total"), title: t("jobs.fields.clm_total"),
dataIndex: "clm_total", dataIndex: "clm_total",

View File

@@ -5,6 +5,7 @@ import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store"; import { store } from "../redux/store";
import axios from "axios"; import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
initializeApp(config); initializeApp(config);
@@ -50,7 +51,7 @@ export { messaging };
export const requestForToken = () => { export const requestForToken = () => {
return getToken(messaging, { return getToken(messaging, {
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
}) })
.then((currentToken) => { .then((currentToken) => {
if (currentToken) { if (currentToken) {
@@ -58,7 +59,9 @@ export const requestForToken = () => {
// Perform any other necessary action with the token // Perform any other necessary action with the token
} else { } else {
// Show permission request UI // Show permission request UI
console.log("No registration token available. Request permission to generate one."); console.log(
"No registration token available. Request permission to generate one."
);
} }
}) })
.catch((err) => { .catch((err) => {
@@ -77,17 +80,24 @@ export const onMessageListener = () =>
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => { export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState(); const state = stateProp || store.getState();
const eventParams = { const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null, shop:
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null, (state.user && state.user.bodyshop && state.user.bodyshop.shopname) ||
...additionalParams null,
user:
(state.user && state.user.currentUser && state.user.currentUser.email) ||
null,
...additionalParams,
}; };
axios.post("/ioevent", { axios.post("/ioevent", {
useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null, useremail:
bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null, (state.user && state.user.currentUser && state.user.currentUser.email) ||
null,
bodyshopid:
(state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName, operationName: eventName,
variables: additionalParams, variables: additionalParams,
dbevent: false, dbevent: false,
env: "master" env: checkBeta() ? "beta" : "master",
}); });
// console.log( // console.log(

View File

@@ -58,7 +58,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
} }
`; `;
export const QUERY_PARTS_BILLS_BY_JOBID = gql` export const QUERY_BILLS_BY_JOBID = gql`
query QUERY_PARTS_BILLS_BY_JOBID($jobid: uuid!) { query QUERY_PARTS_BILLS_BY_JOBID($jobid: uuid!) {
parts_orders( parts_orders(
where: { jobid: { _eq: $jobid } } where: { jobid: { _eq: $jobid } }
@@ -73,7 +73,6 @@ export const QUERY_PARTS_BILLS_BY_JOBID = gql`
order_date order_date
deliver_by deliver_by
return return
returnfrombill
orderedby orderedby
parts_order_lines { parts_order_lines {
id id

View File

@@ -1,7 +1,7 @@
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql` export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
query QUERY_ALL_ACTIVE_JOBS_PAGINATED( query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
$offset: Int $offset: Int
$limit: Int $limit: Int
$order: [jobs_order_by!] $order: [jobs_order_by!]

View File

@@ -71,7 +71,6 @@ export const QUERY_OWNER_BY_ID = gql`
tax_number tax_number
jobs(order_by: { date_open: desc }) { jobs(order_by: { date_open: desc }) {
id id
actual_completion
ro_number ro_number
clm_no clm_no
status status

View File

@@ -30,7 +30,6 @@ export const QUERY_VEHICLE_BY_ID = gql`
notes notes
jobs(order_by: { date_open: desc }) { jobs(order_by: { date_open: desc }) {
id id
actual_completion
ro_number ro_number
ownr_co_nm ownr_co_nm
ownr_fn ownr_fn

View File

@@ -8,7 +8,6 @@ import Icon, {
SyncOutlined, SyncOutlined,
ToolFilled, ToolFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { import {
Button, Button,
Divider, Divider,
@@ -49,7 +48,6 @@ import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gal
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container"; import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container"; import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container"; import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
@@ -80,49 +78,19 @@ export function JobsDetailPage({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const history = useHistory();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory();
const formItemLayout = { const formItemLayout = {
layout: "vertical", layout: "vertical",
}; };
const billsQuery = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
useEffect(() => { useEffect(() => {
//form.setFieldsValue(transormJobToForm(job)); //form.setFieldsValue(transormJobToForm(job));
form.resetFields(); form.resetFields();
}, [form, job]); }, [form, job]);
const handleBillOnRowClick = (record) => {
if (record) {
if (record.id) {
search.billid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.billid;
history.push({ search: queryString.stringify(search) });
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history.push({ search: queryString.stringify(search) });
}
};
//useKeyboardSaveShortcut(form.submit); //useKeyboardSaveShortcut(form.submit);
const handleFinish = async (values) => { const handleFinish = async (values) => {
@@ -318,9 +286,6 @@ export function JobsDetailPage({
<JobsLinesContainer <JobsLinesContainer
job={job} job={job}
joblines={job.joblines} joblines={job.joblines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
refetch={refetch} refetch={refetch}
form={form} form={form}
/> />
@@ -357,12 +322,7 @@ export function JobsDetailPage({
} }
key="partssublet" key="partssublet"
> >
<JobsDetailPliContainer <JobsDetailPliContainer job={job} />
job={job}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
/>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
tab={ tab={

View File

@@ -222,7 +222,6 @@
"onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.", "onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.",
"printlabels": "Print Labels", "printlabels": "Print Labels",
"retailtotal": "Bills Retail Total", "retailtotal": "Bills Retail Total",
"returnfrombill": "Return From Bill",
"savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.", "savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.",
"state_tax": "Provincial/State Tax", "state_tax": "Provincial/State Tax",
"subtotal": "Subtotal", "subtotal": "Subtotal",
@@ -828,11 +827,10 @@
}, },
"status": { "status": {
"in": "Available", "in": "Available",
"inservice": "Service/Maintenance", "inservice": "In Service",
"leasereturn": "Lease Returned", "leasereturn": "Lease Returned",
"out": "Rented", "out": "Rented",
"sold": "Sold", "sold": "Sold"
"unavailable": "Unavailable"
}, },
"successes": { "successes": {
"saved": "Courtesy Car saved successfully." "saved": "Courtesy Car saved successfully."
@@ -887,7 +885,6 @@
"refhrs": "Refinish Hrs" "refhrs": "Refinish Hrs"
}, },
"titles": { "titles": {
"joblifecycle": "Job Lifecycle",
"labhours": "Total Body Hours", "labhours": "Total Body Hours",
"larhours": "Total Refinish Hours", "larhours": "Total Refinish Hours",
"monthlyemployeeefficiency": "Monthly Employee Efficiency", "monthlyemployeeefficiency": "Monthly Employee Efficiency",
@@ -902,7 +899,8 @@
"scheduledindate": "Sheduled In Today: {{date}}", "scheduledindate": "Sheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today", "scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}", "scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today" "scheduledouttoday": "Sheduled Out Today",
"joblifecycle": "Job Lifecycle"
} }
}, },
"dms": { "dms": {
@@ -1238,21 +1236,22 @@
"columns": { "columns": {
"duration": "Duration", "duration": "Duration",
"end": "End", "end": "End",
"human_readable": "Human Readable",
"percentage": "Percentage",
"relative_end": "Relative End", "relative_end": "Relative End",
"relative_start": "Relative Start", "relative_start": "Relative Start",
"start": "Start", "start": "Start",
"value": "Value",
"status": "Status", "status": "Status",
"status_count": "In Status", "percentage": "Percentage",
"value": "Value" "human_readable": "Human Readable",
"status_count": "In Status"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
}, },
"content": { "content": {
"calculated_based_on": "Calculated based on",
"current_status_accumulated_time": "Current Status Accumulated Time", "current_status_accumulated_time": "Current Status Accumulated Time",
"data_unavailable": " There is currently no Lifecycle data for this Job.", "data_unavailable": " There is currently no Lifecycle data for this Job.",
"joblifecycle": "",
"jobs_in_since": "Jobs in since",
"legend_title": "Legend", "legend_title": "Legend",
"loading": "Loading Job Timelines....", "loading": "Loading Job Timelines....",
"not_available": "N/A", "not_available": "N/A",
@@ -1260,14 +1259,12 @@
"title": "Job Lifecycle Component", "title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations", "title_durations": "Historical Status Durations",
"title_loading": "Loading", "title_loading": "Loading",
"title_transitions": "Transitions" "title_transitions": "Transitions",
"calculated_based_on": "Calculated based on",
"jobs_in_since": "Jobs in since"
}, },
"errors": { "errors": {
"fetch": "Error getting Job Lifecycle Data" "fetch": "Error getting Job Lifecycle Data"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
} }
}, },
"job_payments": { "job_payments": {
@@ -1287,7 +1284,6 @@
"amount": "Amount", "amount": "Amount",
"dateOfPayment": "Date of Payment", "dateOfPayment": "Date of Payment",
"descriptions": "Payment Details", "descriptions": "Payment Details",
"hint": "Hint",
"payer": "Payer", "payer": "Payer",
"payername": "Payer Name", "payername": "Payer Name",
"paymentid": "Payment Reference ID", "paymentid": "Payment Reference ID",

View File

@@ -217,12 +217,10 @@
"markexported": "", "markexported": "",
"markforreexport": "", "markforreexport": "",
"new": "", "new": "",
"nobilllines": "",
"noneselected": "", "noneselected": "",
"onlycmforinvoiced": "", "onlycmforinvoiced": "",
"printlabels": "", "printlabels": "",
"retailtotal": "", "retailtotal": "",
"returnfrombill": "",
"savewithdiscrepancy": "", "savewithdiscrepancy": "",
"state_tax": "", "state_tax": "",
"subtotal": "", "subtotal": "",
@@ -831,8 +829,7 @@
"inservice": "", "inservice": "",
"leasereturn": "", "leasereturn": "",
"out": "", "out": "",
"sold": "", "sold": ""
"unavailable": ""
}, },
"successes": { "successes": {
"saved": "" "saved": ""
@@ -887,7 +884,6 @@
"refhrs": "" "refhrs": ""
}, },
"titles": { "titles": {
"joblifecycle": "",
"labhours": "", "labhours": "",
"larhours": "", "larhours": "",
"monthlyemployeeefficiency": "", "monthlyemployeeefficiency": "",
@@ -902,7 +898,8 @@
"scheduledindate": "", "scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "", "scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": "",
"joblifecycle": ""
} }
}, },
"dms": { "dms": {
@@ -1117,7 +1114,6 @@
"loadingshop": "Cargando datos de la tienda ...", "loadingshop": "Cargando datos de la tienda ...",
"loggingin": "Iniciando sesión ...", "loggingin": "Iniciando sesión ...",
"markedexported": "", "markedexported": "",
"media": "",
"message": "", "message": "",
"monday": "", "monday": "",
"na": "N / A", "na": "N / A",
@@ -1238,21 +1234,22 @@
"columns": { "columns": {
"duration": "", "duration": "",
"end": "", "end": "",
"human_readable": "",
"percentage": "",
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "",
"status": "", "status": "",
"status_count": "", "percentage": "",
"value": "" "human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"calculated_based_on": "",
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
"data_unavailable": "", "data_unavailable": "",
"joblifecycle": "",
"jobs_in_since": "",
"legend_title": "", "legend_title": "",
"loading": "", "loading": "",
"not_available": "", "not_available": "",
@@ -1260,14 +1257,12 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "" "title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": ""
}, },
"errors": { "errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo" "fetch": "Error al obtener los datos del ciclo de vida del trabajo"
},
"titles": {
"dashboard": "",
"top_durations": ""
} }
}, },
"job_payments": { "job_payments": {
@@ -1287,7 +1282,6 @@
"amount": "", "amount": "",
"dateOfPayment": "", "dateOfPayment": "",
"descriptions": "", "descriptions": "",
"hint": "",
"payer": "", "payer": "",
"payername": "", "payername": "",
"paymentid": "", "paymentid": "",

View File

@@ -217,12 +217,10 @@
"markexported": "", "markexported": "",
"markforreexport": "", "markforreexport": "",
"new": "", "new": "",
"nobilllines": "",
"noneselected": "", "noneselected": "",
"onlycmforinvoiced": "", "onlycmforinvoiced": "",
"printlabels": "", "printlabels": "",
"retailtotal": "", "retailtotal": "",
"returnfrombill": "",
"savewithdiscrepancy": "", "savewithdiscrepancy": "",
"state_tax": "", "state_tax": "",
"subtotal": "", "subtotal": "",
@@ -831,8 +829,7 @@
"inservice": "", "inservice": "",
"leasereturn": "", "leasereturn": "",
"out": "", "out": "",
"sold": "", "sold": ""
"unavailable": ""
}, },
"successes": { "successes": {
"saved": "" "saved": ""
@@ -887,7 +884,6 @@
"refhrs": "" "refhrs": ""
}, },
"titles": { "titles": {
"joblifecycle": "",
"labhours": "", "labhours": "",
"larhours": "", "larhours": "",
"monthlyemployeeefficiency": "", "monthlyemployeeefficiency": "",
@@ -1117,7 +1113,6 @@
"loadingshop": "Chargement des données de la boutique ...", "loadingshop": "Chargement des données de la boutique ...",
"loggingin": "Vous connecter ...", "loggingin": "Vous connecter ...",
"markedexported": "", "markedexported": "",
"media": "",
"message": "", "message": "",
"monday": "", "monday": "",
"na": "N / A", "na": "N / A",
@@ -1238,21 +1233,22 @@
"columns": { "columns": {
"duration": "", "duration": "",
"end": "", "end": "",
"human_readable": "",
"percentage": "",
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "",
"status": "", "status": "",
"status_count": "", "percentage": "",
"value": "" "human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"calculated_based_on": "",
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
"data_unavailable": "", "data_unavailable": "",
"joblifecycle": "",
"jobs_in_since": "",
"legend_title": "", "legend_title": "",
"loading": "", "loading": "",
"not_available": "", "not_available": "",
@@ -1260,14 +1256,13 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "" "title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": "",
"joblifecycle": ""
}, },
"errors": { "errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches" "fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
},
"titles": {
"dashboard": "",
"top_durations": ""
} }
}, },
"job_payments": { "job_payments": {
@@ -1287,7 +1282,6 @@
"amount": "", "amount": "",
"dateOfPayment": "", "dateOfPayment": "",
"descriptions": "", "descriptions": "",
"hint": "",
"payer": "", "payer": "",
"payername": "", "payername": "",
"paymentid": "", "paymentid": "",

View File

@@ -0,0 +1,37 @@
export const BETA_KEY = 'betaSwitchImex';
export const checkBeta = () => {
const cookie = document.cookie.split('; ').find(row => row.startsWith(BETA_KEY));
return cookie ? cookie.split('=')[1] === 'true' : false;
}
export const setBeta = (value) => {
const domain = window.location.hostname.split('.').slice(-2).join('.');
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
}
export const handleBeta = () => {
// If the current host name does not start with beta or test, then we don't need to do anything.
if (window.location.hostname.startsWith('localhost')) {
console.log('Not on beta or test, so no need to handle beta.');
return;
}
const isBeta = checkBeta();
const currentHostName = window.location.hostname;
// Beta is enabled, but the current host name does start with beta.
if (isBeta && !currentHostName.startsWith('beta')) {
const href= `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Beta is not enabled, but the current host name does start with beta.
else if (!isBeta && currentHostName.startsWith('beta')) {
const href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
}
export default handleBeta;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."tasks" add column "remind_at_sent" timestamptz
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."tasks" add column "remind_at_sent" timestamptz
null;

View File

@@ -224,34 +224,11 @@ exports.default = async function (socket, jobid) {
if (mapaAccount) { if (mapaAccount) {
if (!costCenterHash[mapaAccountName]) if (!costCenterHash[mapaAccountName])
costCenterHash[mapaAccountName] = Dinero(); costCenterHash[mapaAccountName] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) { costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
if (job.mixdata.length > 0) { Dinero(job.job_totals.rates.mapa.total).percentage(
costCenterHash[mapaAccountName] = costCenterHash[ bodyshop?.cdk_configuration?.sendmaterialscosting
mapaAccountName )
].add( );
Dinero({
amount: Math.round(
((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) *
100
),
})
);
} else {
costCenterHash[mapaAccountName] = costCenterHash[
mapaAccountName
].add(
Dinero(job.job_totals.rates.mapa.total).percentage(
bodyshop?.cdk_configuration?.sendmaterialscosting
)
);
}
} else {
costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
Dinero(job.job_totals.rates.mapa.total).percentage(
bodyshop?.cdk_configuration?.sendmaterialscosting
)
);
}
} else { } else {
//console.log("NO MAPA ACCOUNT FOUND!!"); //console.log("NO MAPA ACCOUNT FOUND!!");
} }

View File

@@ -1793,7 +1793,6 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
md_responsibility_centers md_responsibility_centers
cdk_configuration cdk_configuration
pbs_configuration pbs_configuration
use_paint_scale_data
} }
ro_number ro_number
dms_allocation dms_allocation
@@ -1901,10 +1900,6 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
line_ref line_ref
unq_seq unq_seq
} }
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
} }
}`; }`;
@@ -2180,12 +2175,4 @@ exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: c
update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) { update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) {
affected_rows affected_rows
} }
}`; }`;
exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) {
jobs(where: {id: {_in: $ids}}) {
id
shopid
}
}
`;

View File

@@ -8,15 +8,21 @@ const moment = require("moment");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
require("dotenv").config({ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
}); });
const domain = process.env.NODE_ENV ? "secure" : "test"; const domain = process.env.NODE_ENV ? "secure" : "test";
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); const {
SecretsManagerClient,
GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");
const client = new SecretsManagerClient({ const client = new SecretsManagerClient({
region: "ca-central-1" //TODO-AIO: instance manager required when merged to master-AIO region: "ca-central-1",
}); });
const gqlClient = require("../graphql-client/graphql-client").client; const gqlClient = require("../graphql-client/graphql-client").client;
@@ -26,7 +32,7 @@ const getShopCredentials = async (bodyshop) => {
if (process.env.NODE_ENV === undefined) { if (process.env.NODE_ENV === undefined) {
return { return {
merchantkey: process.env.INTELLIPAY_MERCHANTKEY, merchantkey: process.env.INTELLIPAY_MERCHANTKEY,
apikey: process.env.INTELLIPAY_APIKEY apikey: process.env.INTELLIPAY_APIKEY,
}; };
} }
@@ -36,20 +42,26 @@ const getShopCredentials = async (bodyshop) => {
const secret = await client.send( const secret = await client.send(
new GetSecretValueCommand({ new GetSecretValueCommand({
SecretId: `intellipay-credentials-${bodyshop.imexshopid}`, SecretId: `intellipay-credentials-${bodyshop.imexshopid}`,
VersionStage: "AWSCURRENT" // VersionStage defaults to AWSCURRENT if unspecified VersionStage: "AWSCURRENT", // VersionStage defaults to AWSCURRENT if unspecified
}) })
); );
return JSON.parse(secret.SecretString); return JSON.parse(secret.SecretString);
} catch (error) { } catch (error) {
return { return {
error: error.message error: error.message,
}; };
} }
} }
}; };
exports.lightbox_credentials = async (req, res) => { exports.lightbox_credentials = async (req, res) => {
logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, null); logger.log(
"intellipay-lightbox-credentials",
"DEBUG",
req.user?.email,
null,
null
);
const shopCredentials = await getShopCredentials(req.body.bodyshop); const shopCredentials = await getShopCredentials(req.body.bodyshop);
@@ -63,9 +75,11 @@ exports.lightbox_credentials = async (req, res) => {
headers: { "content-type": "application/x-www-form-urlencoded" }, headers: { "content-type": "application/x-www-form-urlencoded" },
data: qs.stringify({ data: qs.stringify({
...shopCredentials, ...shopCredentials,
operatingenv: "businessattended" operatingenv: "businessattended",
}), }),
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal${req.body.refresh ? "_refresh" : ""}` //autoterminal_refresh url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal${
req.body.refresh ? "_refresh" : ""
}`, //autoterminal_refresh
}; };
const response = await axios(options); const response = await axios(options);
@@ -73,9 +87,13 @@ exports.lightbox_credentials = async (req, res) => {
res.send(response.data); res.send(response.data);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
logger.log("intellipay-lightbox-credentials-error", "ERROR", req.user?.email, null, { logger.log(
error: JSON.stringify(error) "intellipay-lightbox-credentials-error",
}); "ERROR",
req.user?.email,
null,
{ error: JSON.stringify(error) }
);
res.json({ error }); res.json({ error });
} }
}; };
@@ -94,9 +112,9 @@ exports.payment_refund = async (req, res) => {
method: "payment_refund", method: "payment_refund",
...shopCredentials, ...shopCredentials,
paymentid: req.body.paymentid, paymentid: req.body.paymentid,
amount: req.body.amount amount: req.body.amount,
}), }),
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund` url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`,
}; };
const response = await axios(options); const response = await axios(options);
@@ -105,7 +123,7 @@ exports.payment_refund = async (req, res) => {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, { logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error) error: JSON.stringify(error),
}); });
res.json({ error }); res.json({ error });
} }
@@ -123,13 +141,15 @@ exports.generate_payment_url = async (req, res) => {
data: qs.stringify({ data: qs.stringify({
...shopCredentials, ...shopCredentials,
//...req.body, //...req.body,
amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"), amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat(
"0.00"
),
account: req.body.account, account: req.body.account,
invoice: req.body.invoice, invoice: req.body.invoice,
createshorturl: true createshorturl: true,
//The postback URL is set at the CP teller global terminal settings page. //The postback URL is set at the CP teller global terminal settings page.
}), }),
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url` url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`,
}; };
const response = await axios(options); const response = await axios(options);
@@ -138,100 +158,56 @@ exports.generate_payment_url = async (req, res) => {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
logger.log("intellipay-payment-url-error", "ERROR", req.user?.email, null, { logger.log("intellipay-payment-url-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error) error: JSON.stringify(error),
}); });
res.json({ error }); res.json({ error });
} }
}; };
exports.postback = async (req, res) => { exports.postback = async (req, res) => {
logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body); logger.log("intellipay-postback", "ERROR", req.user?.email, null, req.body);
const { body: values } = req; const { body: values } = req;
const comment = Buffer.from(values?.comment, "base64").toString(); if (!values.invoice) {
if ((!values.invoice || values.invoice === "") && !comment) {
//invoice is specified through the pay link. Comment by IO.
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, req.body);
res.sendStatus(200); res.sendStatus(200);
return; return;
} }
// TODO query job by account name
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
id: values.invoice,
});
// TODO add mutation to database
try { try {
if (values.invoice) { const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
//This is a link email that's been sent out. paymentInput: {
const job = await gqlClient.request(queries.GET_JOB_BY_PK, { amount: values.total,
id: values.invoice transactionid: `C00 ${values.authcode}`,
}); payer: "Customer",
type: values.cardtype,
jobid: values.invoice,
date: moment(Date.now()),
},
});
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, { await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
paymentInput: { paymentResponse: {
amount: values.total, amount: values.total,
transactionid: values.authcode, bodyshopid: job.jobs_by_pk.shopid,
payer: "Customer", paymentid: paymentResult.id,
type: values.cardtype, jobid: values.invoice,
jobid: values.invoice, declinereason: "Approved",
date: moment(Date.now()) ext_paymentid: values.paymentid,
} successful: true,
}); response: values,
},
});
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, { res.send({ message: "Postback Successful" });
paymentResponse: {
amount: values.total,
bodyshopid: job.jobs_by_pk.shopid,
paymentid: paymentResult.id,
jobid: values.invoice,
declinereason: "Approved",
ext_paymentid: values.paymentid,
successful: true,
response: values
}
});
logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, null, {
iprequest: values,
responseResults,
paymentResult
});
res.sendStatus(200);
} else if (comment) {
//This has been triggered by IO and may have multiple jobs.
const partialPayments = JSON.parse(comment);
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
ids: partialPayments.map((p) => p.jobid)
});
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
paymentInput: partialPayments.map((p) => ({
amount: p.amount,
transactionid: values.authcode,
payer: "Customer",
type: values.cardtype,
jobid: p.jobid,
date: moment(Date.now()),
payment_responses: {
data: {
amount: values.total,
bodyshopid: jobs.jobs[0].shopid,
jobid: p.jobid,
declinereason: "Approved",
ext_paymentid: values.paymentid,
successful: true,
response: values
}
}
}))
});
logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, null, {
iprequest: values,
paymentResult
});
res.sendStatus(200);
}
} catch (error) { } catch (error) {
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, { logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error), error: JSON.stringify(error),
body: req.body body: req.body,
}); });
res.status(400).json({ succesful: false, error: error.message }); res.status(400).json({ succesful: false, error: error.message });
} }