Merge branch 'master' into feature/america
This commit is contained in:
@@ -1,21 +1,38 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Card, Form, Input, InputNumber, Row, Select } from "antd";
|
||||
import moment from "moment";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Spin,
|
||||
notification,
|
||||
} from "antd";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
INSERT_PAYMENT_RESPONSE,
|
||||
QUERY_RO_AND_OWNER_BY_JOB_PK,
|
||||
} from "../../graphql/payment_response.queries";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { connect } from "react-redux";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
@@ -25,24 +42,39 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
const CardPaymentModalComponent = ({
|
||||
bodyshop,
|
||||
context,
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail,
|
||||
}) => {
|
||||
const { context } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const amount = Form.useWatch("amount", form);
|
||||
const payer = Form.useWatch("payer", form);
|
||||
const jobid = Form.useWatch("jobid", form);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, refetch } = useQuery(QUERY_RO_AND_OWNER_BY_JOB_PK, {
|
||||
variables: { jobid: context?.jobid ?? "" },
|
||||
skip: !context?.jobid,
|
||||
});
|
||||
|
||||
const nonApproval = useCallback(
|
||||
async (response) => {
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
window.intellipay.runOnClose(() => {
|
||||
//window.intellipay.initialize();
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
@@ -57,221 +89,193 @@ const CardPaymentModalComponent = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Insert failed payment to audit trail
|
||||
insertAuditTrail({
|
||||
jobid: jobid || context?.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
});
|
||||
},
|
||||
[bodyshop, context, insertAuditTrail, insertPaymentResponse, jobid]
|
||||
);
|
||||
|
||||
const initIntellipayFunctions = useCallback(() => {
|
||||
if (window.intellipay !== undefined && typeof jobid !== "undefined") {
|
||||
console.log("intellipay init functions");
|
||||
|
||||
window.intellipay.runOnClose(() => {
|
||||
window.intellipay.initialize();
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
|
||||
toggleModalVisible();
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(nonApproval);
|
||||
}
|
||||
}, [form, jobid, nonApproval, toggleModalVisible]);
|
||||
|
||||
const initJobId = useCallback(() => {
|
||||
if (context?.jobid) {
|
||||
form.setFieldValue("jobid", context.jobid);
|
||||
}
|
||||
|
||||
form.setFieldValue("payer", t("payments.labels.customer"));
|
||||
}, [context?.jobid, form, t]);
|
||||
|
||||
useEffect(() => {
|
||||
initJobId();
|
||||
|
||||
axios
|
||||
.post("/intellipay/lightbox_credentials", { bodyshop })
|
||||
.then((response) => {
|
||||
var rg = document.createRange();
|
||||
let node = rg.createContextualFragment(response.data);
|
||||
|
||||
document.documentElement.appendChild(node);
|
||||
window.intellipay.initialize();
|
||||
|
||||
initIntellipayFunctions();
|
||||
});
|
||||
|
||||
function handleEvents(...props) {
|
||||
const operation = props[0].data.operation;
|
||||
|
||||
if (operation === "updateform") {
|
||||
props[0].stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleEvents, false);
|
||||
|
||||
return () => window.removeEventListener("message", handleEvents, false);
|
||||
}, [bodyshop, initJobId, initIntellipayFunctions]);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const paymentResult = await insertPayment({
|
||||
variables: {
|
||||
paymentInput: {
|
||||
amount: values.amount,
|
||||
transactionid: values.paymentResponse.receiptelements.transid,
|
||||
payer: values.payer,
|
||||
type: values.paymentResponse.cardType,
|
||||
jobid: values.jobid,
|
||||
date: moment(Date.now()),
|
||||
},
|
||||
},
|
||||
update(cache, { data }) {
|
||||
cache.modify({
|
||||
id: cache.identify({ id: jobid, __typename: "jobs" }),
|
||||
fields: {
|
||||
payments(payments) {
|
||||
return [...data.insert_payments.returning, ...payments];
|
||||
},
|
||||
try {
|
||||
const paymentResult = await insertPayment({
|
||||
variables: {
|
||||
paymentInput: {
|
||||
amount: values.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: values.jobid,
|
||||
date: moment(Date.now()),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: {
|
||||
amount: values.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
paymentid: paymentResult.data.insert_payments.returning[0].id,
|
||||
jobid: values.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
refetchQueries: ["GET_JOB_BY_PK"],
|
||||
update(cache, { data }) {
|
||||
cache.modify({
|
||||
id: cache.identify({ id: values.jobid, __typename: "jobs" }),
|
||||
fields: {
|
||||
payments(cachedPayments) {
|
||||
return [...data.insert_payments.returning, ...cachedPayments];
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: {
|
||||
amount: values.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
paymentid: paymentResult.data.insert_payments.returning[0].id,
|
||||
jobid: values.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse,
|
||||
},
|
||||
},
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.warn("*** Window.Intellipay", !!window.intellipay);
|
||||
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(response.data);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.autoOpen();
|
||||
} else {
|
||||
var rg = document.createRange();
|
||||
let node = rg.createContextualFragment(response.data);
|
||||
document.documentElement.appendChild(node);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.isAutoOpen = true;
|
||||
window.intellipay.initialize();
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip"),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Form onFinish={handleFinish} form={form}>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("bills.fields.ro_number")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<JobSearchSelectComponent
|
||||
disabled={context?.jobid}
|
||||
notExported={false}
|
||||
clm_no
|
||||
onChange={(e) => {
|
||||
refetch({ jobid: e });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
{/* Lighbox Input amount needs to be hidden */}
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="amount"
|
||||
type="hidden"
|
||||
value={amount}
|
||||
hidden
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="account"
|
||||
type="hidden"
|
||||
value={data?.jobs_by_pk.ro_number}
|
||||
hidden
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="email"
|
||||
type="hidden"
|
||||
value={data?.jobs_by_pk.owner.ownr_ea}
|
||||
hidden
|
||||
/>
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" value={amount} />
|
||||
</Form.Item>
|
||||
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("payments.fields.payer")}
|
||||
name="payer"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value={t("payments.labels.customer")}>
|
||||
{t("payments.labels.customer")}
|
||||
</Select.Option>
|
||||
<Select.Option value={t("payments.labels.insurance")}>
|
||||
{t("payments.labels.insurance")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Amount"
|
||||
name="amount"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
<Row justify="space-around">
|
||||
<Button
|
||||
type="primary"
|
||||
data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
disabled={!amount || !payer || !jobid}
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
initialValues={{ jobid: context?.jobid }}
|
||||
>
|
||||
<LayoutFormRow grow noDivider>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("bills.fields.ro_number")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
{context?.balance && (
|
||||
<DataLabel
|
||||
valueStyle={{
|
||||
color: context?.balance.getAmount() !== 0 ? "red" : "green",
|
||||
<JobSearchSelectComponent
|
||||
disabled={context?.jobid}
|
||||
notExported={false}
|
||||
clm_no
|
||||
onChange={(e) => {
|
||||
refetch({ jobid: e });
|
||||
}}
|
||||
label={t("payments.labels.balance")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
{/* Lighbox Input amount needs to be hidden */}
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="amount"
|
||||
type="hidden"
|
||||
value={amount}
|
||||
hidden
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="account"
|
||||
type="hidden"
|
||||
value={data?.jobs_by_pk.ro_number}
|
||||
hidden
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="email"
|
||||
type="hidden"
|
||||
value={data?.jobs_by_pk.owner.ownr_ea}
|
||||
hidden
|
||||
/>
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" value={amount} />
|
||||
</Form.Item>
|
||||
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label="Amount"
|
||||
name="amount"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
// message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
<Row justify="space-around">
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
disabled={!amount || !jobid}
|
||||
onClick={handleIntelliPayCharge}
|
||||
>
|
||||
{context?.balance.toFormat()}
|
||||
</DataLabel>
|
||||
)}
|
||||
</Row>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
{context?.balance && (
|
||||
<DataLabel
|
||||
valueStyle={{
|
||||
color: context?.balance.getAmount() !== 0 ? "red" : "green",
|
||||
}}
|
||||
label={t("payments.labels.balance")}
|
||||
>
|
||||
{context?.balance.toFormat()}
|
||||
</DataLabel>
|
||||
)}
|
||||
</Row>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CardPaymentModalComponent);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CardPaymentModalComponent);
|
||||
|
||||
@@ -22,7 +22,7 @@ function CardPaymentModalContainer({
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
}) {
|
||||
const { context, visible } = cardPaymentModal;
|
||||
const { visible } = cardPaymentModal;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -35,7 +35,7 @@ function CardPaymentModalContainer({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
open={visible}
|
||||
onOk={handleOK}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
@@ -46,7 +46,7 @@ function CardPaymentModalContainer({
|
||||
width="60%"
|
||||
destroyOnClose
|
||||
>
|
||||
<CardPaymentModalComponent bodyshop={bodyshop} context={context} />
|
||||
<CardPaymentModalComponent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,36 +23,40 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
|
||||
const appt = []; // Flatten Data
|
||||
data.scheduled_in_today.forEach((item) => {
|
||||
var i = {
|
||||
canceled: item.canceled,
|
||||
id: item.id,
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
ownr_co_nm: item.job.ownr_co_nm,
|
||||
ownr_ea: item.job.ownr_ea,
|
||||
ownr_fn: item.job.ownr_fn,
|
||||
ownr_ln: item.job.ownr_ln,
|
||||
ownr_ph1: item.job.ownr_ph1,
|
||||
ownr_ph2: item.job.ownr_ph2,
|
||||
production_vars: item.job.production_vars,
|
||||
ro_number: item.job.ro_number,
|
||||
suspended: item.job.suspended,
|
||||
v_make_desc: item.job.v_make_desc,
|
||||
v_model_desc: item.job.v_model_desc,
|
||||
v_model_yr: item.job.v_model_yr,
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: moment(item.start).format("hh:mm a"),
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
if (item.job) {
|
||||
var i = {
|
||||
canceled: item.canceled,
|
||||
id: item.id,
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
ownr_co_nm: item.job.ownr_co_nm,
|
||||
ownr_ea: item.job.ownr_ea,
|
||||
ownr_fn: item.job.ownr_fn,
|
||||
ownr_ln: item.job.ownr_ln,
|
||||
ownr_ph1: item.job.ownr_ph1,
|
||||
ownr_ph2: item.job.ownr_ph2,
|
||||
production_vars: item.job.production_vars,
|
||||
ro_number: item.job.ro_number,
|
||||
suspended: item.job.suspended,
|
||||
v_make_desc: item.job.v_make_desc,
|
||||
v_model_desc: item.job.v_model_desc,
|
||||
v_model_yr: item.job.v_model_yr,
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: moment(item.start).format("hh:mm a"),
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
}
|
||||
});
|
||||
appt.sort(function (a, b) {
|
||||
return new moment(a.start) - new moment(b.start);
|
||||
});
|
||||
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -182,7 +186,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledintoday", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function DataLabel({
|
||||
<div {...props} style={{ display: "flex" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 2,
|
||||
// flex: 2,
|
||||
marginRight: ".2rem",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -97,6 +97,11 @@ function Header({
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
const { ImEXPay } = useTreatments(
|
||||
["ImEXPay"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -243,19 +248,20 @@ function Header({
|
||||
>
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
{/* TODO: Enter Card Payment */}
|
||||
<Menu.Item
|
||||
key="entercardpayments"
|
||||
onClick={() => {
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
context: null,
|
||||
});
|
||||
}}
|
||||
icon={<Icon component={FaCreditCard} />}
|
||||
>
|
||||
{t("menus.header.entercardpayment")}
|
||||
</Menu.Item>
|
||||
{ImEXPay.treatment === "on" && (
|
||||
<Menu.Item
|
||||
key="entercardpayments"
|
||||
onClick={() => {
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
context: null,
|
||||
});
|
||||
}}
|
||||
icon={<Icon component={FaCreditCard} />}
|
||||
>
|
||||
{t("menus.header.entercardpayment")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider key="div5" />
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
@@ -13,13 +14,14 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
|
||||
|
||||
export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
|
||||
export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
@@ -212,7 +214,9 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
|
||||
{!technician ? (
|
||||
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
|
||||
) : null}
|
||||
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
@@ -29,11 +29,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -208,46 +208,56 @@ export function ScheduleEventComponent({
|
||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={event.arrived}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={({ lost_sale_reason }) => {
|
||||
handleCancel({ id: event.id, lost_sale_reason });
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
{event.arrived ? (
|
||||
<Button
|
||||
// onClick={() => handleCancel(event.id)}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={event.arrived}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={({ lost_sale_reason }) => {
|
||||
handleCancel({ id: event.id, lost_sale_reason });
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
// onClick={() => handleCancel(event.id)}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{event.isintake ? (
|
||||
<Button
|
||||
disabled={event.arrived}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Button, Card, Space, Table } from "antd";
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import { Button, Card, Space, Table } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import {
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
|
||||
import {
|
||||
setMessage,
|
||||
openChatByPhone,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import { parsePhoneNumber } from "libphonenumber-js";
|
||||
import axios from "axios";
|
||||
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -46,12 +46,17 @@ export function JobPayments({
|
||||
setCardPaymentContext,
|
||||
refetch,
|
||||
}) {
|
||||
const { ImEXPay } = useTreatments(
|
||||
["ImEXPay"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const [generatingURL, setGeneratingtURL] = useState(false);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -165,39 +170,21 @@ export function JobPayments({
|
||||
title={t("payments.labels.title")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button
|
||||
disabled={!job.converted}
|
||||
loading={generatingURL}
|
||||
onClick={async () => {
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
setGeneratingtURL(true);
|
||||
const response = await axios.post(
|
||||
"/intellipay/generate_payment_url",
|
||||
{
|
||||
bodyshop,
|
||||
amount: balance.getAmount(),
|
||||
account: job.ro_number,
|
||||
{ImEXPay.treatment === "on" && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setCardPaymentContext({
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id, balance },
|
||||
})
|
||||
}
|
||||
);
|
||||
setGeneratingtURL(false);
|
||||
|
||||
console.log("SMS", response);
|
||||
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
});
|
||||
setMessage(
|
||||
t("appointments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: balance.toFormat(),
|
||||
payment_link: response.data.shorUrl,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("menus.header.paymentremindersms")}
|
||||
</Button>
|
||||
>
|
||||
{t("menus.header.entercardpayment")}
|
||||
</Button>
|
||||
<PaymentsGenerateLink job={job} />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
disabled={!job.converted}
|
||||
onClick={() =>
|
||||
@@ -209,16 +196,7 @@ export function JobPayments({
|
||||
>
|
||||
{t("menus.header.enterpayment")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setCardPaymentContext({
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id, balance },
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("menus.header.entercardpayment")}
|
||||
</Button>
|
||||
|
||||
<DataLabel
|
||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||
label={t("payments.labels.balance")}
|
||||
|
||||
@@ -4,16 +4,15 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -173,6 +172,13 @@ export function JobsCloseExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
}
|
||||
if (setSelectedJobs) {
|
||||
setSelectedJobs((selectedJobs) => {
|
||||
return selectedJobs.filter((i) => i !== jobId);
|
||||
|
||||
@@ -143,63 +143,67 @@ export function JobsDetailHeaderActions({
|
||||
<Menu.Item
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
>
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={async ({ lost_sale_reason }) => {
|
||||
const jobUpdate = await cancelAllAppointments({
|
||||
variables: {
|
||||
jobid: job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
lost_sale_reason,
|
||||
status: bodyshop.md_ro_statuses.default_imported,
|
||||
{job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
|
||||
t("menus.jobsactions.cancelallappointments")
|
||||
) : (
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={async ({ lost_sale_reason }) => {
|
||||
const jobUpdate = await cancelAllAppointments({
|
||||
variables: {
|
||||
jobid: job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
lost_sale_reason,
|
||||
status: bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
job.status !== bodyshop.md_ro_statuses.default_scheduled
|
||||
}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
{t("menus.jobsactions.cancelallappointments")}
|
||||
</Popover>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
job.status !== bodyshop.md_ro_statuses.default_scheduled
|
||||
}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
{t("menus.jobsactions.cancelallappointments")}
|
||||
</Popover>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={
|
||||
@@ -245,7 +249,12 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: { jobId: job.id },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
created_by: currentUser.displayName
|
||||
? currentUser.email.concat(" | ", currentUser.displayName)
|
||||
: currentUser.email,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -169,14 +169,20 @@ export function JobsExportAllButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -15,6 +16,15 @@ const mapStateToProps = createStructuredSelector({
|
||||
function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
@@ -26,6 +36,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
@@ -46,11 +59,17 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -60,6 +79,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
),
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -80,6 +102,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
scroll={{ x: true }}
|
||||
rowKey="id"
|
||||
dataSource={owner.jobs}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);
|
||||
|
||||
@@ -113,6 +113,8 @@ export function PartsOrderListTableComponent({
|
||||
id: pol.id,
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
act_price: pol.act_price,
|
||||
oem_partno: pol.oem_partno,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -79,6 +79,20 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.oem_partno")}
|
||||
key={`${index}oem_partno`}
|
||||
name={[field.name, "oem_partno"]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.location")}
|
||||
key={`${index}location`}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_BILLS } from "../../graphql/bills.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import _ from "lodash";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -164,6 +163,13 @@ export function PayableExportAll({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "billsuccessexport",
|
||||
message: t("bills.successes.exported"),
|
||||
});
|
||||
}
|
||||
}
|
||||
})()
|
||||
);
|
||||
@@ -173,7 +179,6 @@ export function PayableExportAll({
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,16 +4,15 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_BILLS } from "../../graphql/bills.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -167,7 +166,13 @@ export function PayableExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "billsuccessexport",
|
||||
message: t("bills.successes.exported"),
|
||||
});
|
||||
}
|
||||
|
||||
if (setSelectedBills) {
|
||||
setSelectedBills((selectedBills) => {
|
||||
@@ -177,6 +182,7 @@ export function PayableExportButton({
|
||||
}
|
||||
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -172,8 +172,15 @@ export function PaymentExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "paymentsuccessexport",
|
||||
message: t("payments.successes.exported"),
|
||||
});
|
||||
}
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export function PaymentsExportAllButton({
|
||||
disabled,
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch
|
||||
refetch,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
|
||||
@@ -150,6 +150,13 @@ export function PaymentsExportAllButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "paymentsuccessexport",
|
||||
message: t("payments.successes.exported"),
|
||||
});
|
||||
}
|
||||
}
|
||||
})()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { CopyFilled } from "@ant-design/icons";
|
||||
import { Button, Form, Popover, Space, message } from "antd";
|
||||
import axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import { parsePhoneNumber } from "libphonenumber-js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text)),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PaymentsGenerateLink);
|
||||
|
||||
export function PaymentsGenerateLink({
|
||||
bodyshop,
|
||||
callback,
|
||||
job,
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [paymentLink, setPaymentLink] = useState(null);
|
||||
|
||||
const handleFinish = async ({ amount }) => {
|
||||
setLoading(true);
|
||||
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
setLoading(true);
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: amount,
|
||||
account: job.ro_number,
|
||||
invoice: job.id,
|
||||
});
|
||||
setLoading(false);
|
||||
setPaymentLink(response.data.shorUrl);
|
||||
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl,
|
||||
})
|
||||
);
|
||||
|
||||
//Add in confirmation & errors.
|
||||
if (callback) callback();
|
||||
|
||||
// setVisible(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish} layout="vertical" form={form}>
|
||||
<Form.Item
|
||||
label={t("payments.fields.amount")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name="amount"
|
||||
>
|
||||
<CurrencyFormItemComponent />
|
||||
</Form.Item>
|
||||
{paymentLink && (
|
||||
<Space direction="vertical">
|
||||
<Space
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(paymentLink);
|
||||
message.success(t("general.actions.copied"));
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
//Copy the link.
|
||||
}}
|
||||
>
|
||||
{paymentLink}
|
||||
</div>{" "}
|
||||
<CopyFilled />
|
||||
</Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: Dinero({
|
||||
amount: Math.round(form.getFieldValue("amount") * 100),
|
||||
}).toFormat(),
|
||||
payment_link: paymentLink,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("general.actions.sendbysms")}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form>
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setPaymentLink(null);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button onClick={() => setVisible(true)} loading={loading}>
|
||||
{t("payments.actions.generatepaymentlink")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { selectPrintCenter } from "../../redux/modals/modals.selectors";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
printCenterModal: selectPrintCenter,
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
@@ -22,6 +24,7 @@ export function PrintCenterItemComponent({
|
||||
id,
|
||||
bodyshop,
|
||||
disabled,
|
||||
technician,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { context } = printCenterModal;
|
||||
@@ -44,19 +47,24 @@ export function PrintCenterItemComponent({
|
||||
<Space wrap>
|
||||
{item.title}
|
||||
<PrinterOutlined onClick={renderToNewWindow} />
|
||||
<MailOutlined
|
||||
onClick={() => {
|
||||
GenerateDocument(
|
||||
{
|
||||
name: item.key,
|
||||
variables: { id: id },
|
||||
},
|
||||
{ to: context.job && context.job.ownr_ea, subject: item.subject },
|
||||
"e",
|
||||
id
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!technician ? (
|
||||
<MailOutlined
|
||||
onClick={() => {
|
||||
GenerateDocument(
|
||||
{
|
||||
name: item.key,
|
||||
variables: { id: id },
|
||||
},
|
||||
{
|
||||
to: context.job && context.job.ownr_ea,
|
||||
subject: item.subject,
|
||||
},
|
||||
"e",
|
||||
id
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{loading && <Spin />}
|
||||
</Space>
|
||||
</li>
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Descriptions, Drawer, Space, PageHeader, Button } from "antd";
|
||||
import { Button, Descriptions, Drawer, PageHeader, Space } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import JobAtChange from "../job-at-change/job-at-change.component";
|
||||
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
|
||||
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
|
||||
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
|
||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
||||
import JobAtChange from "../job-at-change/job-at-change.component";
|
||||
import { PrinterFilled } from "@ant-design/icons";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) =>
|
||||
@@ -40,6 +42,7 @@ export function ProductionListDetail({
|
||||
bodyshop,
|
||||
jobs,
|
||||
setPrintCenterContext,
|
||||
technician,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useHistory();
|
||||
@@ -66,7 +69,9 @@ export function ProductionListDetail({
|
||||
title={theJob.ro_number}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<ProductionRemoveButton jobId={theJob.id} />{" "}
|
||||
{!technician ? (
|
||||
<ProductionRemoveButton jobId={theJob.id} />
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPrintCenterContext({
|
||||
@@ -82,7 +87,9 @@ export function ProductionListDetail({
|
||||
<PrinterFilled />
|
||||
{t("jobs.actions.printCenter")}
|
||||
</Button>
|
||||
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
|
||||
{!technician ? (
|
||||
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -59,11 +59,12 @@ export function ScheduleManualEvent({ bodyshop, event }) {
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
|
||||
});
|
||||
}
|
||||
form.resetFields();
|
||||
setVisibility(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export function TechClockInContainer({
|
||||
>
|
||||
{t("timetickets.actions.enter")}
|
||||
</Button>
|
||||
<TechJobPrintTickets />
|
||||
<TechJobPrintTickets attendacePrint={false} />
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => form.submit()}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Card, DatePicker, Form, Popover, Space } from "antd";
|
||||
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -21,12 +21,13 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(TechJobPrintTickets);
|
||||
|
||||
export function TechJobPrintTickets({ technician, event }) {
|
||||
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const Templates = TemplateList("report_center");
|
||||
|
||||
useEffect(() => {
|
||||
if (visibility && event) {
|
||||
@@ -44,7 +45,10 @@ export function TechJobPrintTickets({ technician, event }) {
|
||||
try {
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: TemplateList().timetickets_employee.key,
|
||||
name:
|
||||
attendacePrint === true
|
||||
? Templates.attendance_employee.key
|
||||
: Templates.timetickets_employee.key,
|
||||
variables: {
|
||||
...(start
|
||||
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
|
||||
@@ -60,9 +64,12 @@ export function TechJobPrintTickets({ technician, event }) {
|
||||
},
|
||||
{
|
||||
to: technician.email,
|
||||
subject: TemplateList().timetickets_employee.subject,
|
||||
subject:
|
||||
attendacePrint === true
|
||||
? Templates.attendance_employee.subject
|
||||
: Templates.timetickets_employee.subject,
|
||||
},
|
||||
"p"
|
||||
values.sendby // === "email" ? "e" : "p"
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -92,10 +99,25 @@ export function TechJobPrintTickets({ technician, event }) {
|
||||
format={"MM/DD/YYYY"}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item dependencies={["dates"]}>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendby"
|
||||
initialValue="p"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="e">{t("general.labels.email")}</Radio>
|
||||
<Radio value="p">{t("general.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.print")}
|
||||
{t("reportcenter.actions.generate")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -118,7 +140,7 @@ export function TechJobPrintTickets({ technician, event }) {
|
||||
return (
|
||||
<Popover content={overlay} visible={visibility}>
|
||||
<Button loading={loading} onClick={handleClick}>
|
||||
{t("general.actions.print")}
|
||||
{t("general.labels.reports")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -4,48 +4,67 @@ import { useTranslation } from "react-i18next";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
|
||||
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
||||
|
||||
export default function TimeTicketShiftActive({ timetickets, refetch }) {
|
||||
export default function TimeTicketShiftActive({
|
||||
timetickets,
|
||||
refetch,
|
||||
isTechConsole,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{timetickets.length > 0 ? (
|
||||
<div>
|
||||
<Typography.Title level={2}>
|
||||
{t("timetickets.labels.shiftalreadyclockedon")}
|
||||
</Typography.Title>
|
||||
<List
|
||||
grid={{
|
||||
gutter: 32,
|
||||
xs: 1,
|
||||
sm: 2,
|
||||
md: 3,
|
||||
lg: 4,
|
||||
xl: 5,
|
||||
xxl: 6,
|
||||
}}
|
||||
dataSource={timetickets || []}
|
||||
renderItem={(ticket) => (
|
||||
<List.Item>
|
||||
<Card
|
||||
title={t(ticket.memo)}
|
||||
actions={[
|
||||
<TechClockOffButton
|
||||
jobId={ticket.jobid}
|
||||
timeTicketId={ticket.id}
|
||||
completedCallback={refetch}
|
||||
isShiftTicket
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<DataLabel label={t("timetickets.fields.clockon")}>
|
||||
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
|
||||
</DataLabel>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
></List>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography.Title level={2}>
|
||||
{t("timetickets.labels.shiftalreadyclockedon")}
|
||||
</Typography.Title>
|
||||
{isTechConsole ? (
|
||||
<TechJobPrintTickets attendacePrint={true} />
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<List
|
||||
grid={{
|
||||
gutter: 32,
|
||||
xs: 1,
|
||||
sm: 2,
|
||||
md: 3,
|
||||
lg: 4,
|
||||
xl: 5,
|
||||
xxl: 6,
|
||||
}}
|
||||
dataSource={timetickets || []}
|
||||
renderItem={(ticket) => (
|
||||
<List.Item>
|
||||
<Card
|
||||
title={t(ticket.memo)}
|
||||
actions={[
|
||||
<TechClockOffButton
|
||||
jobId={ticket.jobid}
|
||||
timeTicketId={ticket.id}
|
||||
completedCallback={refetch}
|
||||
isShiftTicket
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<DataLabel label={t("timetickets.fields.clockon")}>
|
||||
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
|
||||
</DataLabel>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
></List>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Form, notification } from "antd";
|
||||
import { Button, Form, Space, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import React, { useMemo, useState } from "react";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
||||
import TimeTicektShiftComponent from "./time-ticket-shift-form.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -29,6 +30,10 @@ export function TimeTicektShiftContainer({
|
||||
isTechConsole,
|
||||
checkIfAlreadyClocked,
|
||||
}) {
|
||||
console.log(
|
||||
"🚀 ~ file: time-ticket-shift-form.container.jsx:28 ~ technician:",
|
||||
technician
|
||||
);
|
||||
const [form] = Form.useForm();
|
||||
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
||||
const { t } = useTranslation();
|
||||
@@ -113,9 +118,14 @@ export function TimeTicektShiftContainer({
|
||||
initialValues={{ cost_center: t("timetickets.labels.shift") }}
|
||||
>
|
||||
<TimeTicektShiftComponent form={form} />
|
||||
<Button htmlType="submit" loading={loading}>
|
||||
{t("timetickets.actions.clockin")}
|
||||
</Button>
|
||||
<Space wrap>
|
||||
<Button htmlType="submit" loading={loading} type="primary">
|
||||
{t("timetickets.actions.clockin")}
|
||||
</Button>
|
||||
{isTechConsole === true ? (
|
||||
<TechJobPrintTickets attendacePrint={true} />
|
||||
) : null}
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,6 +76,7 @@ export function TimeTicketShiftContainer({
|
||||
<TimeTicketShiftActive
|
||||
timetickets={data ? data.timetickets : []}
|
||||
refetch={refetch}
|
||||
isTechConsole={isTechConsole}
|
||||
/>
|
||||
) : (
|
||||
<TimeTicketShiftFormContainer
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -16,6 +17,14 @@ const mapStateToProps = createStructuredSelector({
|
||||
export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -28,6 +37,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
@@ -43,11 +55,17 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -57,6 +75,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
),
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,6 +97,7 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
rowKey="id"
|
||||
scroll={{ x: true }}
|
||||
dataSource={vehicle.jobs}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);
|
||||
|
||||
Reference in New Issue
Block a user