Reformat all project files to use the prettier config file.
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import React, {useEffect} from "react";
|
||||
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||
import React, { useEffect } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
function PrivateRoute({ component: Component, isAuthorized, ...rest }) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthorized) {
|
||||
navigate(`/signin?redirect=${location.pathname}`);
|
||||
}
|
||||
}, [isAuthorized, navigate, location]);
|
||||
useEffect(() => {
|
||||
if (!isAuthorized) {
|
||||
navigate(`/signin?redirect=${location.pathname}`);
|
||||
}
|
||||
}, [isAuthorized, navigate, location]);
|
||||
|
||||
return <Outlet/>;
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import {Button} from "antd";
|
||||
import { Button } from "antd";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setRefundPaymentContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "refund_payment"})),
|
||||
setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
|
||||
});
|
||||
|
||||
function Test({setRefundPaymentContext, refundPaymentModal}) {
|
||||
console.log("refundPaymentModal", refundPaymentModal);
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setRefundPaymentContext({
|
||||
context: {},
|
||||
})
|
||||
}
|
||||
>
|
||||
Open Modal
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
function Test({ setRefundPaymentContext, refundPaymentModal }) {
|
||||
console.log("refundPaymentModal", refundPaymentModal);
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setRefundPaymentContext({
|
||||
context: {}
|
||||
})
|
||||
}
|
||||
>
|
||||
Open Modal
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect } from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
@@ -17,216 +18,179 @@ import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AccountingPayablesTableComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
|
||||
|
||||
export function AccountingPayablesTableComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
bills,
|
||||
refetch,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/shop/vendors`,
|
||||
search: queryString.stringify({selectedvendor: record.vendor.id}),
|
||||
}}
|
||||
>
|
||||
{record.vendor.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "invoice_number" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/bills`,
|
||||
search: queryString.stringify({
|
||||
billid: record.id,
|
||||
vendorid: record.vendor.id,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{record.invoice_number}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.is_credit_memo}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => (
|
||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
|
||||
|
||||
render: (text, record) => (
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedBills={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({...state, search: e.target.value});
|
||||
logImEXEvent("accounting_payables_table_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? bills.filter(
|
||||
(v) =>
|
||||
(v.vendor.name || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.invoice_number || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
)
|
||||
: bills;
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<BillMarkSelectedExported
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<PayableExportAll
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||
<QboAuthorizeComponent/>
|
||||
)}
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/shop/vendors`,
|
||||
search: queryString.stringify({ selectedvendor: record.vendor.id })
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{position: "top", pageSize: pageLimit}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedBills(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedBills(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedBills,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
{record.vendor.name}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/bills`,
|
||||
search: queryString.stringify({
|
||||
billid: record.id,
|
||||
vendorid: record.vendor.id
|
||||
})
|
||||
}}
|
||||
>
|
||||
{record.invoice_number}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox disabled checked={record.is_credit_memo} />
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
|
||||
render: (text, record) => (
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedBills={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("accounting_payables_table_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? bills.filter(
|
||||
(v) =>
|
||||
(v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: bills;
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<BillMarkSelectedExported
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<PayableExportAll
|
||||
billids={selectedBills}
|
||||
disabled={transInProgress || selectedBills.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
refetch={refetch}
|
||||
/>
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedBills(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported
|
||||
}),
|
||||
selectedRowKeys: selectedBills,
|
||||
type: "checkbox"
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,242 +1,198 @@
|
||||
import {Card, Input, Space, Table} from "antd";
|
||||
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 {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { Card, Input, Space, Table } from "antd";
|
||||
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 { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {pageLimit } from "../../utils/config";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AccountingPayablesTableComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
|
||||
|
||||
export function AccountingPayablesTableComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
payments,
|
||||
refetch,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => <Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number}</Link>
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
<OwnerNameDisplay ownerObject={record.job}/>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record.job}/>
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
sorter: (a, b) => a.amount - b.amount,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => (
|
||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.memo"),
|
||||
dataIndex: "memo",
|
||||
key: "memo",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.transactionid"),
|
||||
dataIndex: "transactionid",
|
||||
key: "transactionid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
//{
|
||||
// title: t("payments.fields.exportedat"),
|
||||
// dataIndex: "exportedat",
|
||||
// key: "exportedat",
|
||||
// render: (text, record) => (
|
||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
// ),
|
||||
//},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
sorter: (a, b) => a.amount - b.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.memo"),
|
||||
dataIndex: "memo",
|
||||
key: "memo"
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.transactionid"),
|
||||
dataIndex: "transactionid",
|
||||
key: "transactionid"
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
},
|
||||
//{
|
||||
// title: t("payments.fields.exportedat"),
|
||||
// dataIndex: "exportedat",
|
||||
// key: "exportedat",
|
||||
// render: (text, record) => (
|
||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
// ),
|
||||
//},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => (
|
||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
|
||||
render: (text, record) => (
|
||||
<PaymentExportButton
|
||||
paymentId={record.id}
|
||||
disabled={transInProgress || !!record.exportedat}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedPayments={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
render: (text, record) => (
|
||||
<PaymentExportButton
|
||||
paymentId={record.id}
|
||||
disabled={transInProgress || !!record.exportedat}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedPayments={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("account_payments_table_search");
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({...state, search: e.target.value});
|
||||
logImEXEvent("account_payments_table_search");
|
||||
};
|
||||
const dataSource = state.search
|
||||
? payments.filter(
|
||||
(v) =>
|
||||
(v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: payments;
|
||||
|
||||
const dataSource = state.search
|
||||
? payments.filter(
|
||||
(v) =>
|
||||
(v.paymentnum || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ro_number) || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_fn) || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_ln) || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
((v.job && v.job.ownr_co_nm) || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
)
|
||||
: payments;
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<PaymentMarkSelectedExported
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress || selectedPayments.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<PaymentsExportAllButton
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress || selectedPayments.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||
<QboAuthorizeComponent/>
|
||||
)}
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{position: "top", pageSize: pageLimit}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedPayments,
|
||||
type: "checkbox",
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<PaymentMarkSelectedExported
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress || selectedPayments.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<PaymentsExportAllButton
|
||||
paymentIds={selectedPayments}
|
||||
disabled={transInProgress || selectedPayments.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedPayments}
|
||||
refetch={refetch}
|
||||
/>
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported
|
||||
}),
|
||||
selectedRowKeys: selectedPayments,
|
||||
type: "checkbox"
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,258 +1,212 @@
|
||||
import {Button, Card, Input, Space, Table} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AccountingReceivablesTableComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
||||
|
||||
export function AccountingReceivablesTableComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
jobs,
|
||||
refetch,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
||||
),
|
||||
},
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => <Link to={"/manage/jobs/" + record.id}>{record.ro_number}</Link>
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.date_invoiced"),
|
||||
dataIndex: "date_invoiced",
|
||||
key: "date_invoiced",
|
||||
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date_invoiced" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.date_invoiced}</DateFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.owner ? (
|
||||
<Link to={"/manage/owners/" + record.owner.id}>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.date_invoiced"),
|
||||
dataIndex: "date_invoiced",
|
||||
key: "date_invoiced",
|
||||
sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
|
||||
sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date_invoiced}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.owner ? (
|
||||
<Link to={"/manage/owners/" + record.owner.id}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
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 || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
render: (text, record) => (
|
||||
<ExportLogsCountDisplay logs={record.exportlogs}/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
render: (text, record) => <ExportLogsCountDisplay logs={record.exportlogs} />
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<JobExportButton
|
||||
jobId={record.id}
|
||||
disabled={!!record.date_exported}
|
||||
setSelectedJobs={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<JobExportButton
|
||||
jobId={record.id}
|
||||
disabled={!!record.date_exported}
|
||||
setSelectedJobs={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||
</Link>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({...state, search: e.target.value});
|
||||
logImEXEvent("accounting_receivables_search");
|
||||
};
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
logImEXEvent("accounting_receivables_search");
|
||||
};
|
||||
|
||||
const dataSource = state.search
|
||||
? jobs.filter(
|
||||
(v) =>
|
||||
(v.ro_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_co_nm || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: jobs;
|
||||
const dataSource = state.search
|
||||
? jobs.filter(
|
||||
(v) =>
|
||||
(v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: jobs;
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||
<QboAuthorizeComponent/>
|
||||
)}
|
||||
<Input.Search
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{position: "top"}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) =>
|
||||
setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported,
|
||||
}),
|
||||
selectedRowKeys: selectedJobs,
|
||||
type: "checkbox",
|
||||
}}
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
<Input.Search
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: record.exported
|
||||
}),
|
||||
selectedRowKeys: selectedJobs,
|
||||
type: "checkbox"
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Alert} from "antd";
|
||||
import { Alert } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function AlertComponent(props) {
|
||||
return <Alert {...props} />;
|
||||
return <Alert {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {shallow} from "enzyme";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Alert from "./alert.component";
|
||||
|
||||
describe("Alert component", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message.",
|
||||
};
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
type: "error",
|
||||
message: "Test error message."
|
||||
};
|
||||
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
});
|
||||
wrapper = shallow(<Alert {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render Alert component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("should render Alert component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,72 +1,67 @@
|
||||
import {Button, InputNumber, Popover, Select} from "antd";
|
||||
import { Button, InputNumber, Popover, Select } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function AllocationsAssignmentComponent({
|
||||
bodyshop,
|
||||
handleAssignment,
|
||||
assignment,
|
||||
setAssignment,
|
||||
visibilityState,
|
||||
maxHours
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
bodyshop,
|
||||
handleAssignment,
|
||||
assignment,
|
||||
setAssignment,
|
||||
visibilityState,
|
||||
maxHours
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChange = e => {
|
||||
setAssignment({...assignment, employeeid: e});
|
||||
};
|
||||
const onChange = (e) => {
|
||||
setAssignment({ ...assignment, employeeid: e });
|
||||
};
|
||||
|
||||
const [visibility, setVisibility] = visibilityState;
|
||||
const [visibility, setVisibility] = visibilityState;
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Select id="employeeSelector"
|
||||
showSearch
|
||||
style={{width: 200}}
|
||||
placeholder='Select a person'
|
||||
optionFilterProp='children'
|
||||
onChange={onChange}
|
||||
filterOption={(input, option) =>
|
||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}>
|
||||
{bodyshop.employees.map(emp => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<InputNumber
|
||||
defaultValue={assignment.hours}
|
||||
placeholder={t("joblines.fields.mod_lb_hrs")}
|
||||
max={parseFloat(maxHours)}
|
||||
min={0}
|
||||
onChange={e => setAssignment({...assignment, hours: e})}
|
||||
/>
|
||||
const popContent = (
|
||||
<div>
|
||||
<Select
|
||||
id="employeeSelector"
|
||||
showSearch
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select a person"
|
||||
optionFilterProp="children"
|
||||
onChange={onChange}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
>
|
||||
{bodyshop.employees.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<InputNumber
|
||||
defaultValue={assignment.hours}
|
||||
placeholder={t("joblines.fields.mod_lb_hrs")}
|
||||
max={parseFloat(maxHours)}
|
||||
min={0}
|
||||
onChange={(e) => setAssignment({ ...assignment, hours: e })}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='primary'
|
||||
disabled={!assignment.employeeid}
|
||||
onClick={handleAssignment}>
|
||||
Assign
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</div>
|
||||
);
|
||||
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
|
||||
Assign
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Button onClick={() => setVisibility(true)}>
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Button onClick={() => setVisibility(true)}>{t("allocations.actions.assign")}</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import {mount} from "enzyme";
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import {MockBodyshop} from "../../utils/TestingHelpers";
|
||||
import {AllocationsAssignmentComponent} from "./allocations-assignment.component";
|
||||
import { MockBodyshop } from "../../utils/TestingHelpers";
|
||||
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
|
||||
|
||||
describe("AllocationsAssignmentComponent component", () => {
|
||||
let wrapper;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
bodyshop: MockBodyshop,
|
||||
handleAssignment: jest.fn(),
|
||||
assignment: {},
|
||||
setAssignment: jest.fn(),
|
||||
visibilityState: [false, jest.fn()],
|
||||
maxHours: 4,
|
||||
};
|
||||
beforeEach(() => {
|
||||
const mockProps = {
|
||||
bodyshop: MockBodyshop,
|
||||
handleAssignment: jest.fn(),
|
||||
assignment: {},
|
||||
setAssignment: jest.fn(),
|
||||
visibilityState: [false, jest.fn()],
|
||||
maxHours: 4
|
||||
};
|
||||
|
||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||
});
|
||||
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
|
||||
});
|
||||
|
||||
it("should render AllocationsAssignmentComponent component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("should render AllocationsAssignmentComponent component", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render a list of employees", () => {
|
||||
const empList = wrapper.find("#employeeSelector");
|
||||
expect(empList.children()).to.have.lengthOf(2);
|
||||
});
|
||||
it("should render a list of employees", () => {
|
||||
const empList = wrapper.find("#employeeSelector");
|
||||
expect(empList.children()).to.have.lengthOf(2);
|
||||
});
|
||||
|
||||
it("should create an allocation on save", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("should create an allocation on save", () => {
|
||||
wrapper.find("Button").simulate("click");
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
import React, {useState} from "react";
|
||||
import React, { useState } from "react";
|
||||
import AllocationsAssignmentComponent from "./allocations-assignment.component";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {notification} from "antd";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { notification } from "antd";
|
||||
|
||||
export default function AllocationsAssignmentContainer({
|
||||
jobLineId,
|
||||
hours,
|
||||
refetch,
|
||||
}) {
|
||||
const visibilityState = useState(false);
|
||||
const {t} = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
joblineid: jobLineId,
|
||||
hours: parseFloat(hours),
|
||||
employeeid: null,
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
|
||||
const visibilityState = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
joblineid: jobLineId,
|
||||
hours: parseFloat(hours),
|
||||
employeeid: null
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
|
||||
const handleAssignment = () => {
|
||||
insertAllocation({variables: {alloc: {...assignment}}})
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.save"),
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("employees.errors.saving", {message: error.message}),
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleAssignment = () => {
|
||||
insertAllocation({ variables: { alloc: { ...assignment } } })
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.save")
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("employees.errors.saving", { message: error.message })
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AllocationsAssignmentComponent
|
||||
handleAssignment={handleAssignment}
|
||||
maxHours={hours}
|
||||
assignment={assignment}
|
||||
setAssignment={setAssignment}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<AllocationsAssignmentComponent
|
||||
handleAssignment={handleAssignment}
|
||||
maxHours={hours}
|
||||
assignment={assignment}
|
||||
setAssignment={setAssignment}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,62 @@
|
||||
import {Button, Popover, Select} from "antd";
|
||||
import { Button, Popover, Select } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
mapStateToProps,
|
||||
null
|
||||
)(function AllocationsBulkAssignmentComponent({
|
||||
disabled,
|
||||
bodyshop,
|
||||
handleAssignment,
|
||||
assignment,
|
||||
setAssignment,
|
||||
visibilityState,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
disabled,
|
||||
bodyshop,
|
||||
handleAssignment,
|
||||
assignment,
|
||||
setAssignment,
|
||||
visibilityState
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChange = (e) => {
|
||||
setAssignment({...assignment, employeeid: e});
|
||||
};
|
||||
const onChange = (e) => {
|
||||
setAssignment({ ...assignment, employeeid: e });
|
||||
};
|
||||
|
||||
const [visibility, setVisibility] = visibilityState;
|
||||
const [visibility, setVisibility] = visibilityState;
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 200}}
|
||||
placeholder="Select a person"
|
||||
optionFilterProp="children"
|
||||
onChange={onChange}
|
||||
filterOption={(input, option) =>
|
||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{bodyshop.employees.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
const popContent = (
|
||||
<div>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select a person"
|
||||
optionFilterProp="children"
|
||||
onChange={onChange}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
>
|
||||
{bodyshop.employees.map((emp) => (
|
||||
<Select.Option value={emp.id} key={emp.id}>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!assignment.employeeid}
|
||||
onClick={handleAssignment}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</div>
|
||||
);
|
||||
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
|
||||
Assign
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
import React, {useState} from "react";
|
||||
import React, { useState } from "react";
|
||||
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {notification} from "antd";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { notification } from "antd";
|
||||
|
||||
export default function AllocationsBulkAssignmentContainer({
|
||||
jobLines,
|
||||
refetch,
|
||||
}) {
|
||||
const visibilityState = useState(false);
|
||||
const {t} = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
employeeid: null,
|
||||
export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
|
||||
const visibilityState = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [assignment, setAssignment] = useState({
|
||||
employeeid: null
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
|
||||
const handleAssignment = () => {
|
||||
const allocs = jobLines.reduce((acc, value) => {
|
||||
acc.push({
|
||||
joblineid: value.id,
|
||||
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||
employeeid: assignment.employeeid
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
insertAllocation({ variables: { alloc: allocs } }).then((r) => {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save")
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
});
|
||||
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
|
||||
};
|
||||
|
||||
const handleAssignment = () => {
|
||||
const allocs = jobLines.reduce((acc, value) => {
|
||||
acc.push({
|
||||
joblineid: value.id,
|
||||
hours: parseFloat(value.mod_lb_hrs) || 0,
|
||||
employeeid: assignment.employeeid,
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
insertAllocation({variables: {alloc: allocs}}).then((r) => {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.save"),
|
||||
});
|
||||
visibilityState[1](false);
|
||||
if (refetch) refetch();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AllocationsBulkAssignment
|
||||
disabled={jobLines.length > 0 ? false : true}
|
||||
handleAssignment={handleAssignment}
|
||||
assignment={assignment}
|
||||
setAssignment={setAssignment}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<AllocationsBulkAssignment
|
||||
disabled={jobLines.length > 0 ? false : true}
|
||||
handleAssignment={handleAssignment}
|
||||
assignment={assignment}
|
||||
setAssignment={setAssignment}
|
||||
visibilityState={visibilityState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {MdRemoveCircleOutline} from "react-icons/md";
|
||||
import { MdRemoveCircleOutline } from "react-icons/md";
|
||||
|
||||
export default function AllocationsLabelComponent({allocation, handleClick}) {
|
||||
return (
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
export default function AllocationsLabelComponent({ allocation, handleClick }) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>
|
||||
{`${allocation.employee.first_name || ""} ${
|
||||
allocation.employee.last_name || ""
|
||||
} (${allocation.hours || ""})`}
|
||||
{`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
|
||||
</span>
|
||||
<Icon
|
||||
style={{color: "red", padding: "0px 4px"}}
|
||||
component={MdRemoveCircleOutline}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<Icon style={{ color: "red", padding: "0px 4px" }} component={MdRemoveCircleOutline} onClick={handleClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
import React from "react";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {DELETE_ALLOCATION} from "../../graphql/allocations.queries";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
|
||||
import AllocationsLabelComponent from "./allocations-employee-label.component";
|
||||
import {notification} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { notification } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AllocationsLabelContainer({allocation, refetch}) {
|
||||
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
|
||||
const {t} = useTranslation();
|
||||
export default function AllocationsLabelContainer({ allocation, refetch }) {
|
||||
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
deleteAllocation({variables: {id: allocation.id}})
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.deleted"),
|
||||
});
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({message: t("allocations.errors.deleting")});
|
||||
});
|
||||
};
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
deleteAllocation({ variables: { id: allocation.id } })
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("allocations.successes.deleted")
|
||||
});
|
||||
if (refetch) refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({ message: t("allocations.errors.deleting") });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AllocationsLabelComponent
|
||||
allocation={allocation}
|
||||
handleClick={handleClick}
|
||||
/>
|
||||
);
|
||||
return <AllocationsLabelComponent allocation={allocation} handleClick={handleClick} />;
|
||||
}
|
||||
|
||||
@@ -1,85 +1,75 @@
|
||||
import React, {useState} from "react";
|
||||
import {Table} from "antd";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, { useState } from "react";
|
||||
import { Table } from "antd";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
export default function AuditTrailListComponent({loading, data}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const {t} = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
key: " created",
|
||||
width: "10%",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => a.created - b.created,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.operation"),
|
||||
dataIndex: "operation",
|
||||
key: "operation",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.values"),
|
||||
dataIndex: " old_val",
|
||||
key: " old_val",
|
||||
width: "10%",
|
||||
render: (text, record) => (
|
||||
<AuditTrailValuesComponent
|
||||
oldV={record.old_val}
|
||||
newV={record.new_val}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.useremail"),
|
||||
dataIndex: "useremail",
|
||||
key: "useremail",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
export default function AuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {}
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
key: " created",
|
||||
width: "10%",
|
||||
render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
|
||||
sorter: (a, b) => a.created - b.created,
|
||||
sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.operation"),
|
||||
dataIndex: "operation",
|
||||
key: "operation",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.operation, b.operation),
|
||||
sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.values"),
|
||||
dataIndex: " old_val",
|
||||
key: " old_val",
|
||||
width: "10%",
|
||||
render: (text, record) => <AuditTrailValuesComponent oldV={record.old_val} newV={record.new_val} />
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.useremail"),
|
||||
dataIndex: "useremail",
|
||||
key: "useremail",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||
sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 12},
|
||||
sm: {span: 5},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 12},
|
||||
},
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: { span: 12 },
|
||||
sm: { span: 5 }
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 12 }
|
||||
}
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
import React from "react";
|
||||
import AuditTrailListComponent from "./audit-trail-list.component";
|
||||
import {useQuery} from "@apollo/client";
|
||||
import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
|
||||
import {Card, Row} from "antd";
|
||||
import { Card, Row } from "antd";
|
||||
|
||||
export default function AuditTrailListContainer({recordId}) {
|
||||
const {loading, error, data} = useQuery(QUERY_AUDIT_TRAIL, {
|
||||
variables: {id: recordId},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
export default function AuditTrailListContainer({ recordId }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||
variables: { id: recordId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
logImEXEvent("audittrail_view", {recordId});
|
||||
return (
|
||||
<div>
|
||||
{error ? (
|
||||
<AlertComponent type="error" message={error.message}/>
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Card>
|
||||
<AuditTrailListComponent
|
||||
loading={loading}
|
||||
data={data ? data.audit_trail : []}
|
||||
/>
|
||||
</Card>
|
||||
<Card>
|
||||
<EmailAuditTrailListComponent
|
||||
loading={loading}
|
||||
data={data ? data.audit_trail : []}
|
||||
/>
|
||||
</Card>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
logImEXEvent("audittrail_view", { recordId });
|
||||
return (
|
||||
<div>
|
||||
{error ? (
|
||||
<AlertComponent type="error" message={error.message} />
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Card>
|
||||
<AuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
|
||||
</Card>
|
||||
<Card>
|
||||
<EmailAuditTrailListComponent loading={loading} data={data ? data.audit_trail : []} />
|
||||
</Card>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,60 @@
|
||||
import {Table} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
export default function EmailAuditTrailListComponent({loading, data}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const {t} = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
key: " created",
|
||||
width: "10%",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => a.created - b.created,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
|
||||
},
|
||||
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {}
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
key: " created",
|
||||
width: "10%",
|
||||
render: (text, record) => <DateTimeFormatter>{record.created}</DateTimeFormatter>,
|
||||
sorter: (a, b) => a.created - b.created,
|
||||
sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
|
||||
},
|
||||
|
||||
{
|
||||
title: t("audit.fields.useremail"),
|
||||
dataIndex: "useremail",
|
||||
key: "useremail",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
{
|
||||
title: t("audit.fields.useremail"),
|
||||
dataIndex: "useremail",
|
||||
key: "useremail",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
|
||||
sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 12},
|
||||
sm: {span: 5},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 12},
|
||||
},
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: { span: 12 },
|
||||
sm: { span: 5 }
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 12 }
|
||||
}
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{position: "top", defaultPageSize: pageLimit}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import React from "react";
|
||||
import {List} from "antd";
|
||||
import { List } from "antd";
|
||||
import Icon from "@ant-design/icons";
|
||||
import {FaArrowRight} from "react-icons/fa";
|
||||
import { FaArrowRight } from "react-icons/fa";
|
||||
|
||||
export default function AuditTrailValuesComponent({oldV, newV}) {
|
||||
if (!oldV && !newV) return <div></div>;
|
||||
|
||||
if (!oldV && newV)
|
||||
return (
|
||||
<List style={{width: "800px"}} bordered size='small'>
|
||||
{Object.keys(newV).map((key, idx) => (
|
||||
<List.Item key={idx} value={key}>
|
||||
{key}: {JSON.stringify(newV[key])}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
export default function AuditTrailValuesComponent({ oldV, newV }) {
|
||||
if (!oldV && !newV) return <div></div>;
|
||||
|
||||
if (!oldV && newV)
|
||||
return (
|
||||
<List style={{width: "800px"}} bordered size='small'>
|
||||
{Object.keys(oldV).map((key, idx) => (
|
||||
<List.Item key={idx}>
|
||||
{key}: {oldV[key]} <Icon component={FaArrowRight}/>
|
||||
{JSON.stringify(newV[key])}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
<List style={{ width: "800px" }} bordered size="small">
|
||||
{Object.keys(newV).map((key, idx) => (
|
||||
<List.Item key={idx} value={key}>
|
||||
{key}: {JSON.stringify(newV[key])}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
return (
|
||||
<List style={{ width: "800px" }} bordered size="small">
|
||||
{Object.keys(oldV).map((key, idx) => (
|
||||
<List.Item key={idx}>
|
||||
{key}: {oldV[key]} <Icon component={FaArrowRight} />
|
||||
{JSON.stringify(newV[key])}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import {Popover, Tag} from "antd";
|
||||
import { Popover, Tag } from "antd";
|
||||
import React from "react";
|
||||
import Barcode from "react-barcode";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function BarcodePopupComponent({value, children}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Popover
|
||||
content={
|
||||
<Barcode
|
||||
value={value || ""}
|
||||
background="transparent"
|
||||
displayValue={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
export default function BarcodePopupComponent({ value, children }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Popover content={<Barcode value={value || ""} background="transparent" displayValue={false} />}>
|
||||
{children ? children : <Tag>{t("general.labels.barcode")}</Tag>}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,136 +1,128 @@
|
||||
import {Checkbox, Form, Skeleton, Typography} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||
import "./bill-cm-returns-table.styles.scss";
|
||||
|
||||
export default function BillCmdReturnsTableComponent({
|
||||
form,
|
||||
returnLoading,
|
||||
returnData,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (returnData) {
|
||||
form.setFieldsValue({
|
||||
outstanding_returns: returnData.parts_order_lines,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (returnData) {
|
||||
form.setFieldsValue({
|
||||
outstanding_returns: returnData.parts_order_lines
|
||||
});
|
||||
}
|
||||
}, [returnData, form]);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) =>
|
||||
prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid
|
||||
}
|
||||
noStyle
|
||||
>
|
||||
{() => {
|
||||
const isReturn = form.getFieldValue("is_credit_memo");
|
||||
|
||||
if (!isReturn) {
|
||||
return null;
|
||||
}
|
||||
}, [returnData, form]);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) =>
|
||||
prev.jobid !== cur.jobid ||
|
||||
prev.is_credit_memo !== cur.is_credit_memo ||
|
||||
prev.vendorid !== cur.vendorid
|
||||
}
|
||||
noStyle
|
||||
>
|
||||
{() => {
|
||||
const isReturn = form.getFieldValue("is_credit_memo");
|
||||
if (returnLoading) return <Skeleton />;
|
||||
|
||||
if (!isReturn) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Form.List name="outstanding_returns">
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>{t("bills.labels.creditsnotreceived")}</Typography.Title>
|
||||
<table className="bill-cm-returns-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("parts_orders.fields.line_desc")}</th>
|
||||
<th>{t("parts_orders.fields.part_type")}</th>
|
||||
<th>{t("parts_orders.fields.quantity")}</th>
|
||||
<th>{t("parts_orders.fields.act_price")}</th>
|
||||
<th>{t("parts_orders.fields.cost")}</th>
|
||||
<th>{t("parts_orders.labels.mark_as_received")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
if (returnLoading) return <Skeleton/>;
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "part_type"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cost`}
|
||||
name={[field.name, "cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
return (
|
||||
<Form.List name="outstanding_returns">
|
||||
{(fields, {add, remove, move}) => {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("bills.labels.creditsnotreceived")}
|
||||
</Typography.Title>
|
||||
<table className="bill-cm-returns-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("parts_orders.fields.line_desc")}</th>
|
||||
<th>{t("parts_orders.fields.part_type")}</th>
|
||||
<th>{t("parts_orders.fields.quantity")}</th>
|
||||
<th>{t("parts_orders.fields.act_price")}</th>
|
||||
<th>{t("parts_orders.fields.cost")}</th>
|
||||
<th>{t("parts_orders.labels.mark_as_received")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "part_type"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cost`}
|
||||
name={[field.name, "cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cm_received`}
|
||||
name={[field.name, "cm_received"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
);
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cm_received`}
|
||||
name={[field.name, "cm_received"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
</Form.List>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,88 @@
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, notification, Popconfirm} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {DELETE_BILL} from "../../graphql/bills.queries";
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||
|
||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
const result = await deleteBill({
|
||||
variables: {billId: bill.id},
|
||||
update(cache, {errors}) {
|
||||
if (errors) return;
|
||||
cache.modify({
|
||||
fields: {
|
||||
bills(existingBills, {readField}) {
|
||||
return existingBills.filter(
|
||||
(billref) => bill.id !== readField("id", billref)
|
||||
);
|
||||
},
|
||||
search_bills(existingBills, {readField}) {
|
||||
return existingBills.filter(
|
||||
(billref) => bill.id !== readField("id", billref)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
const result = await deleteBill({
|
||||
variables: { billId: bill.id },
|
||||
update(cache, { errors }) {
|
||||
if (errors) return;
|
||||
cache.modify({
|
||||
fields: {
|
||||
bills(existingBills, { readField }) {
|
||||
return existingBills.filter((billref) => bill.id !== readField("id", billref));
|
||||
},
|
||||
search_bills(existingBills, { readField }) {
|
||||
return existingBills.filter((billref) => bill.id !== readField("id", billref));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
type: "billdeleted",
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
type: "billdeleted"
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
//Check if it's an fkey violation.
|
||||
const error = JSON.stringify(result.errors);
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
//Check if it's an fkey violation.
|
||||
const error = JSON.stringify(result.errors);
|
||||
|
||||
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: t("bills.errors.existinginventoryline"),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: t("bills.errors.existinginventoryline")
|
||||
})
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.deleting", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<RbacWrapper action="bills:delete" noauth={<></>}>
|
||||
<Popconfirm
|
||||
disabled={bill.exported}
|
||||
onConfirm={handleDelete}
|
||||
title={t("bills.labels.deleteconfirm")}
|
||||
>
|
||||
<Button
|
||||
disabled={bill.exported}
|
||||
// onClick={handleDelete}
|
||||
loading={loading}
|
||||
>
|
||||
<DeleteFilled/>
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</RbacWrapper>
|
||||
);
|
||||
return (
|
||||
<RbacWrapper action="bills:delete" noauth={<></>}>
|
||||
<Popconfirm disabled={bill.exported} onConfirm={handleDelete} title={t("bills.labels.deleteconfirm")}>
|
||||
<Button
|
||||
disabled={bill.exported}
|
||||
// onClick={handleDelete}
|
||||
loading={loading}
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {useMutation, useQuery} from "@apollo/client";
|
||||
import {Button, Divider, Form, Popconfirm, Space} from "antd";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE} from "../../graphql/bill-lines.queries";
|
||||
import {QUERY_BILL_BY_PK, UPDATE_BILL} from "../../graphql/bills.queries";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE } from "../../graphql/bill-lines.queries";
|
||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
@@ -22,227 +22,212 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||
import {PageHeader} from "@ant-design/pro-layout";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillDetailEditcontainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
|
||||
|
||||
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [update_bill] = useMutation(UPDATE_BILL);
|
||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [update_bill] = useMutation(UPDATE_BILL);
|
||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||
|
||||
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
|
||||
variables: {billid: search.billid},
|
||||
skip: !!!search.billid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
||||
variables: { billid: search.billid },
|
||||
skip: !!!search.billid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
// ... rest of the code remains the same
|
||||
// ... rest of the code remains the same
|
||||
|
||||
const handleSave = () => {
|
||||
//It's got a previously deducted bill line!
|
||||
if (
|
||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
||||
0
|
||||
)
|
||||
setOpen(true);
|
||||
else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
const handleSave = () => {
|
||||
//It's got a previously deducted bill line!
|
||||
if (
|
||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0
|
||||
)
|
||||
setOpen(true);
|
||||
else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
//let adjustmentsToInsert = {};
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
//let adjustmentsToInsert = {};
|
||||
|
||||
const {billlines, upload, ...bill} = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
update_bill({
|
||||
variables: {billId: search.billid, bill: bill},
|
||||
})
|
||||
);
|
||||
|
||||
billlines.forEach((l) => {
|
||||
delete l.selected;
|
||||
});
|
||||
|
||||
//Find bill lines that were deleted.
|
||||
const deletedJobLines = [];
|
||||
|
||||
data.bills_by_pk.billlines.forEach((a) => {
|
||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||
if (!matchingRecord) {
|
||||
deletedJobLines.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
deletedJobLines.forEach((d) => {
|
||||
updates.push(deleteBillLine({variables: {id: d.id}}));
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const {deductedfromlbr, inventories, jobline, ...il} = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
updates.push(
|
||||
updateBillLine({
|
||||
variables: {
|
||||
billLineId: il.id,
|
||||
billLine: {
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
//It's a new line, have to insert it.
|
||||
updates.push(
|
||||
insertBillLine({
|
||||
variables: {
|
||||
billLines: [
|
||||
{
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
billid: search.billid,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
type: "billupdated",
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transformData(data));
|
||||
form.resetFields();
|
||||
setOpen(false);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LoadingSkeleton/>}
|
||||
{data && (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
data &&
|
||||
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<BillDetailEditReturn data={data}/>
|
||||
<BillPrintButton billid={search.billid}/>
|
||||
<Popconfirm
|
||||
open={open}
|
||||
onConfirm={() => form.submit()}
|
||||
onCancel={() => setOpen(false)}
|
||||
okButtonProps={{loading: updateLoading}}
|
||||
title={t("bills.labels.editadjwarning")}
|
||||
>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={exported}
|
||||
onClick={handleSave}
|
||||
loading={updateLoading}
|
||||
type="primary"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<BillReeportButtonComponent bill={data && data.bills_by_pk}/>
|
||||
<BillMarkExportedButton bill={data && data.bills_by_pk}/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={transformData(data)}
|
||||
layout="vertical"
|
||||
>
|
||||
<BillFormContainer form={form} billEdit disabled={exported}/>
|
||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
job={{id: data ? data.bills_by_pk.jobid : null}}
|
||||
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||
/>
|
||||
) : (
|
||||
<JobDocumentsGallery
|
||||
jobId={data ? data.bills_by_pk.jobid : null}
|
||||
billId={search.billid}
|
||||
documentsList={data ? data.bills_by_pk.documents : []}
|
||||
billsCallback={refetch}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
const { billlines, upload, ...bill } = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
update_bill({
|
||||
variables: { billId: search.billid, bill: bill }
|
||||
})
|
||||
);
|
||||
|
||||
billlines.forEach((l) => {
|
||||
delete l.selected;
|
||||
});
|
||||
|
||||
//Find bill lines that were deleted.
|
||||
const deletedJobLines = [];
|
||||
|
||||
data.bills_by_pk.billlines.forEach((a) => {
|
||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||
if (!matchingRecord) {
|
||||
deletedJobLines.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
deletedJobLines.forEach((d) => {
|
||||
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
updates.push(
|
||||
updateBillLine({
|
||||
variables: {
|
||||
billLineId: il.id,
|
||||
billLine: {
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
//It's a new line, have to insert it.
|
||||
updates.push(
|
||||
insertBillLine({
|
||||
variables: {
|
||||
billLines: [
|
||||
{
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
billid: search.billid,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
type: "billupdated"
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transformData(data));
|
||||
form.resetFields();
|
||||
setOpen(false);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LoadingSkeleton />}
|
||||
{data && (
|
||||
<>
|
||||
<PageHeader
|
||||
title={data && `${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`}
|
||||
extra={
|
||||
<Space>
|
||||
<BillDetailEditReturn data={data} />
|
||||
<BillPrintButton billid={search.billid} />
|
||||
<Popconfirm
|
||||
open={open}
|
||||
onConfirm={() => form.submit()}
|
||||
onCancel={() => setOpen(false)}
|
||||
okButtonProps={{ loading: updateLoading }}
|
||||
title={t("bills.labels.editadjwarning")}
|
||||
>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={exported}
|
||||
onClick={handleSave}
|
||||
loading={updateLoading}
|
||||
type="primary"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
||||
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||
/>
|
||||
) : (
|
||||
<JobDocumentsGallery
|
||||
jobId={data ? data.bills_by_pk.jobid : null}
|
||||
billId={search.billid}
|
||||
documentsList={data ? data.bills_by_pk.documents : []}
|
||||
billsCallback={refetch}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const transformData = (data) => {
|
||||
return data
|
||||
? {
|
||||
...data.bills_by_pk,
|
||||
return data
|
||||
? {
|
||||
...data.bills_by_pk,
|
||||
|
||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
|
||||
}
|
||||
: {};
|
||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false
|
||||
}
|
||||
};
|
||||
}),
|
||||
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null
|
||||
}
|
||||
: {};
|
||||
};
|
||||
|
||||
@@ -1,189 +1,168 @@
|
||||
import {Button, Checkbox, Form, Modal} from "antd";
|
||||
import { Button, Checkbox, Form, Modal } 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, useNavigate} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillDetailEditReturn);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||
|
||||
export function BillDetailEditReturn({
|
||||
setPartsOrderContext,
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
data,
|
||||
disabled,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleFinish = ({billlines}) => {
|
||||
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
||||
const handleFinish = ({ billlines }) => {
|
||||
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
||||
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
linesToOrder: data.bills_by_pk.billlines
|
||||
.filter((l) => selectedLines.includes(l.id))
|
||||
.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
cost: i.actual_cost,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||
part_type: i.jobline && i.jobline.part_type,
|
||||
};
|
||||
}),
|
||||
isReturn: true,
|
||||
},
|
||||
});
|
||||
delete search.billid;
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
linesToOrder: data.bills_by_pk.billlines
|
||||
.filter((l) => selectedLines.includes(l.id))
|
||||
.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
cost: i.actual_cost,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||
part_type: i.jobline && i.jobline.part_type
|
||||
};
|
||||
}),
|
||||
isReturn: true
|
||||
}
|
||||
});
|
||||
delete search.billid;
|
||||
|
||||
history({search: queryString.stringify(search)});
|
||||
setOpen(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (open === false) form.resetFields();
|
||||
}, [open, form]);
|
||||
history({ search: queryString.stringify(search) });
|
||||
setOpen(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (open === false) form.resetFields();
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
title={t("bills.actions.return")}
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form
|
||||
initialValues={data && data.bills_by_pk}
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
>
|
||||
<Form.List name={["billlines"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
return (
|
||||
<table style={{tableLayout: "auto", width: "100%"}}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<Checkbox
|
||||
onChange={(e) => {
|
||||
form.setFieldsValue({
|
||||
billlines: form
|
||||
.getFieldsValue()
|
||||
.billlines.map((b) => ({
|
||||
...b,
|
||||
selected: e.target.checked,
|
||||
})),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{t("billlines.fields.line_desc")}</td>
|
||||
<td>{t("billlines.fields.quantity")}</td>
|
||||
<td>{t("billlines.fields.actual_price")}</td>
|
||||
<td>{t("billlines.fields.actual_cost")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.selected")}
|
||||
key={`${index}selected`}
|
||||
name={[field.name, "selected"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_price")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_cost")}
|
||||
key={`${index}actual_cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Button
|
||||
disabled={
|
||||
data.bills_by_pk.is_credit_memo ||
|
||||
data.bills_by_pk.isinhouse ||
|
||||
disabled
|
||||
}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
title={t("bills.actions.return")}
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form initialValues={data && data.bills_by_pk} onFinish={handleFinish} form={form}>
|
||||
<Form.List name={["billlines"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<table style={{ tableLayout: "auto", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<Checkbox
|
||||
onChange={(e) => {
|
||||
form.setFieldsValue({
|
||||
billlines: form.getFieldsValue().billlines.map((b) => ({
|
||||
...b,
|
||||
selected: e.target.checked
|
||||
}))
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{t("billlines.fields.line_desc")}</td>
|
||||
<td>{t("billlines.fields.quantity")}</td>
|
||||
<td>{t("billlines.fields.actual_price")}</td>
|
||||
<td>{t("billlines.fields.actual_cost")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.selected")}
|
||||
key={`${index}selected`}
|
||||
name={[field.name, "selected"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_price")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_cost")}
|
||||
key={`${index}actual_cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Button
|
||||
disabled={data.bills_by_pk.is_credit_memo || data.bills_by_pk.isinhouse || disabled}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
import {Drawer, Grid} from "antd";
|
||||
import { Drawer, Grid } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import BillDetailEditComponent from "./bill-detail-edit-component";
|
||||
|
||||
export default function BillDetailEditcontainer() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "100%",
|
||||
xl: "90%",
|
||||
xxl: "90%",
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint
|
||||
? bpoints[selectedBreakpoint[0]]
|
||||
: "100%";
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "100%",
|
||||
xl: "90%",
|
||||
xxl: "90%"
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width={drawerPercentage}
|
||||
onClose={() => {
|
||||
delete search.billid;
|
||||
history({search: queryString.stringify(search)});
|
||||
}}
|
||||
destroyOnClose
|
||||
open={search.billid}
|
||||
>
|
||||
<BillDetailEditComponent/>
|
||||
</Drawer>
|
||||
);
|
||||
return (
|
||||
<Drawer
|
||||
width={drawerPercentage}
|
||||
onClose={() => {
|
||||
delete search.billid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
destroyOnClose
|
||||
open={search.billid}
|
||||
>
|
||||
<BillDetailEditComponent />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,471 +1,426 @@
|
||||
import {useApolloClient, useMutation} from "@apollo/client";
|
||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||
import {Button, Checkbox, Form, Modal, notification, Space} from "antd";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_NEW_BILL} from "../../graphql/bills.queries";
|
||||
import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries";
|
||||
import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries";
|
||||
import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries";
|
||||
import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
|
||||
import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility";
|
||||
import {handleUpload} from "../documents-upload/documents-upload.utility";
|
||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
billEnterModal: selectBillEnterModal,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
billEnterModal: selectBillEnterModal,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
insertAuditTrail: ({jobid, billid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, billid, operation, type })),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation, type }))
|
||||
});
|
||||
|
||||
const Templates = TemplateList("job_special");
|
||||
|
||||
function BillEnterModalContainer({
|
||||
billEnterModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
currentUser,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const {t} = useTranslation();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
const [generateLabel, setGenerateLabel] = useLocalStorage(
|
||||
"enter_bill_generate_label",
|
||||
false
|
||||
);
|
||||
function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
|
||||
|
||||
const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid,
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
return {
|
||||
...billEnterModal.context.bill,
|
||||
//Added as a part of IO-2436 for capturing parts price changes.
|
||||
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
|
||||
...line,
|
||||
original_actual_price: line.actual_price
|
||||
})),
|
||||
jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||
federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0,
|
||||
state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0,
|
||||
local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0
|
||||
};
|
||||
}, [billEnterModal, bodyshop]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
let totals = CalculateBillTotal(values);
|
||||
if (totals.discrepancy.getAmount() !== 0) {
|
||||
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
|
||||
|
||||
let adjustmentsToInsert = {};
|
||||
let payrollAdjustmentsToInsert = [];
|
||||
|
||||
const r1 = await insertBill({
|
||||
variables: {
|
||||
bill: [
|
||||
{
|
||||
...remainingValues,
|
||||
billlines: {
|
||||
data:
|
||||
remainingValues.billlines &&
|
||||
remainingValues.billlines.map((i) => {
|
||||
const {
|
||||
deductedfromlbr,
|
||||
lbr_adjustment,
|
||||
location: lineLocation,
|
||||
part_type,
|
||||
create_ppc,
|
||||
original_actual_price,
|
||||
...restI
|
||||
} = i;
|
||||
|
||||
if (Enhanced_Payroll.treatment === "on") {
|
||||
if (
|
||||
deductedfromlbr &&
|
||||
true //payroll is on
|
||||
) {
|
||||
payrollAdjustmentsToInsert.push({
|
||||
id: i.joblineid,
|
||||
convertedtolbr: true,
|
||||
convertedtolbr_data: {
|
||||
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
|
||||
mod_lbr_ty: lbr_adjustment.mod_lbr_ty
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (deductedfromlbr) {
|
||||
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
|
||||
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
|
||||
restI.actual_price / lbr_adjustment.rate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...restI,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
lbr_adjustment,
|
||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||
applicable_taxes: {
|
||||
federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
||||
awaitRefetchQueries: true
|
||||
});
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
return {
|
||||
...billEnterModal.context.bill,
|
||||
//Added as a part of IO-2436 for capturing parts price changes.
|
||||
billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
|
||||
...line,
|
||||
original_actual_price: line.actual_price,
|
||||
})),
|
||||
jobid:
|
||||
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||
federal_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
||||
0,
|
||||
state_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
||||
0,
|
||||
local_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
||||
0,
|
||||
};
|
||||
}, [billEnterModal, bodyshop]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
let totals = CalculateBillTotal(values);
|
||||
if (totals.discrepancy.getAmount() !== 0) {
|
||||
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||
return;
|
||||
await Promise.all(
|
||||
payrollAdjustmentsToInsert.map((li) => {
|
||||
return updateJobLines({
|
||||
variables: {
|
||||
lineId: li.id,
|
||||
line: {
|
||||
convertedtolbr: li.convertedtolbr,
|
||||
convertedtolbr_data: li.convertedtolbr_data
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const {
|
||||
upload,
|
||||
location,
|
||||
outstanding_returns,
|
||||
inventory,
|
||||
federal_tax_exempt,
|
||||
...remainingValues
|
||||
} = values;
|
||||
|
||||
let adjustmentsToInsert = {};
|
||||
let payrollAdjustmentsToInsert = [];
|
||||
|
||||
const r1 = await insertBill({
|
||||
variables: {
|
||||
bill: [
|
||||
{
|
||||
...remainingValues,
|
||||
billlines: {
|
||||
data:
|
||||
remainingValues.billlines &&
|
||||
remainingValues.billlines.map((i) => {
|
||||
const {
|
||||
deductedfromlbr,
|
||||
lbr_adjustment,
|
||||
location: lineLocation,
|
||||
part_type,
|
||||
create_ppc,
|
||||
original_actual_price,
|
||||
...restI
|
||||
} = i;
|
||||
|
||||
if (Enhanced_Payroll.treatment === "on") {
|
||||
if (
|
||||
deductedfromlbr &&
|
||||
true //payroll is on
|
||||
) {
|
||||
payrollAdjustmentsToInsert.push({
|
||||
id: i.joblineid,
|
||||
convertedtolbr: true,
|
||||
convertedtolbr_data: {
|
||||
mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
|
||||
mod_lbr_ty: lbr_adjustment.mod_lbr_ty,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (deductedfromlbr) {
|
||||
adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
|
||||
(adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
|
||||
restI.actual_price / lbr_adjustment.rate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...restI,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
lbr_adjustment,
|
||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
||||
false,
|
||||
state:
|
||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
||||
false,
|
||||
local:
|
||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
||||
false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
||||
awaitRefetchQueries: true
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
payrollAdjustmentsToInsert.map((li) => {
|
||||
return updateJobLines({
|
||||
variables: {
|
||||
lineId: li.id,
|
||||
line: {
|
||||
convertedtolbr: li.convertedtolbr,
|
||||
convertedtolbr_data: li.convertedtolbr_data,
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||
if (adjKeys.length > 0) {
|
||||
//Query the adjustments, merge, and update them.
|
||||
const existingAdjustments = await client.query({
|
||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
variables: {
|
||||
id: values.jobid,
|
||||
},
|
||||
});
|
||||
|
||||
const newAdjustments = _.cloneDeep(
|
||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
adjKeys.forEach((key) => {
|
||||
newAdjustments[key] =
|
||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
type: "jobmodifylbradj",});
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB,
|
||||
variables: {
|
||||
jobId: values.jobid,
|
||||
job: {lbr_adjustments: newAdjustments},
|
||||
},
|
||||
});
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||
if (adjKeys.length > 0) {
|
||||
//Query the adjustments, merge, and update them.
|
||||
const existingAdjustments = await client.query({
|
||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
variables: {
|
||||
id: values.jobid
|
||||
}
|
||||
});
|
||||
|
||||
const markPolReceived =
|
||||
outstanding_returns &&
|
||||
outstanding_returns.filter((o) => o.cm_received === true);
|
||||
const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
|
||||
|
||||
if (markPolReceived && markPolReceived.length > 0) {
|
||||
const r2 = await updatePartsOrderLines({
|
||||
variables: {partsLineIds: markPolReceived.map((p) => p.id)},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID" ],
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.updating", {
|
||||
message: JSON.stringify(r2.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!r1.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.creating", {
|
||||
message: JSON.stringify(r1.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const billId = r1.data.insert_bills.returning[0].id;
|
||||
const markInventoryConsumed =
|
||||
inventory && inventory.filter((i) => i.consumefrominventory);
|
||||
|
||||
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
||||
const r2 = await updateInventoryLines({
|
||||
variables: {
|
||||
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
||||
consumedbybillid: billId,
|
||||
},
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("inventory.errors.updating", {
|
||||
message: JSON.stringify(r2.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
//If it's not a credit memo, update the statuses.
|
||||
|
||||
if (!values.is_credit_memo) {
|
||||
await Promise.all(
|
||||
remainingValues.billlines
|
||||
.filter((il) => il.joblineid !== "noline")
|
||||
.map((li) => {
|
||||
return updateJobLines({
|
||||
variables: {
|
||||
lineId: li.joblineid,
|
||||
line: {
|
||||
location: li.location || location,
|
||||
status:
|
||||
bodyshop.md_order_statuses.default_received || "Received*",
|
||||
//Added parts price changes.
|
||||
...(li.create_ppc &&
|
||||
li.original_actual_price !== li.actual_price
|
||||
? {
|
||||
act_price_before_ppc: li.original_actual_price,
|
||||
act_price: li.actual_price,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
if (upload && upload.length > 0) {
|
||||
//insert Each of the documents?
|
||||
|
||||
if (bodyshop.uselocalmediaserver) {
|
||||
upload.forEach((u) => {
|
||||
handleLocalUpload({
|
||||
ev: {file: u.originFileObj},
|
||||
context: {
|
||||
jobid: values.jobid,
|
||||
invoice_number: remainingValues.invoice_number,
|
||||
vendorid: remainingValues.vendorid,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
upload.forEach((u) => {
|
||||
handleUpload(
|
||||
{file: u.originFileObj},
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
///////////////////////////
|
||||
setLoading(false);
|
||||
notification["success"]({
|
||||
message: t("bills.successes.created"),
|
||||
});
|
||||
|
||||
if (generateLabel) {
|
||||
GenerateDocument(
|
||||
{
|
||||
name: Templates.parts_invoice_label_single.key,
|
||||
variables: {
|
||||
id: billId,
|
||||
},
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
}
|
||||
|
||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||
adjKeys.forEach((key) => {
|
||||
newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
billid: billId,
|
||||
operation: AuditTrailMapping.billposted(
|
||||
r1.data.insert_bills.returning[0].invoice_number
|
||||
),
|
||||
type: "billposted",
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1)
|
||||
}),
|
||||
type: "jobmodifylbradj"
|
||||
});
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB,
|
||||
variables: {
|
||||
jobId: values.jobid,
|
||||
job: { lbr_adjustments: newAdjustments }
|
||||
}
|
||||
});
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true);
|
||||
|
||||
if (markPolReceived && markPolReceived.length > 0) {
|
||||
const r2 = await updatePartsOrderLines({
|
||||
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.updating", {
|
||||
message: JSON.stringify(r2.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!r1.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.creating", {
|
||||
message: JSON.stringify(r1.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const billId = r1.data.insert_bills.returning[0].id;
|
||||
const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory);
|
||||
|
||||
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
||||
const r2 = await updateInventoryLines({
|
||||
variables: {
|
||||
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
||||
consumedbybillid: billId
|
||||
}
|
||||
});
|
||||
if (!!r2.errors) {
|
||||
setLoading(false);
|
||||
setEnterAgain(false);
|
||||
notification["error"]({
|
||||
message: t("inventory.errors.updating", {
|
||||
message: JSON.stringify(r2.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
//If it's not a credit memo, update the statuses.
|
||||
|
||||
if (!values.is_credit_memo) {
|
||||
await Promise.all(
|
||||
remainingValues.billlines
|
||||
.filter((il) => il.joblineid !== "noline")
|
||||
.map((li) => {
|
||||
return updateJobLines({
|
||||
variables: {
|
||||
lineId: li.joblineid,
|
||||
line: {
|
||||
location: li.location || location,
|
||||
status: bodyshop.md_order_statuses.default_received || "Received*",
|
||||
//Added parts price changes.
|
||||
...(li.create_ppc && li.original_actual_price !== li.actual_price
|
||||
? {
|
||||
act_price_before_ppc: li.original_actual_price,
|
||||
act_price: li.actual_price
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
if (upload && upload.length > 0) {
|
||||
//insert Each of the documents?
|
||||
|
||||
if (bodyshop.uselocalmediaserver) {
|
||||
upload.forEach((u) => {
|
||||
handleLocalUpload({
|
||||
ev: { file: u.originFileObj },
|
||||
context: {
|
||||
jobid: values.jobid,
|
||||
invoice_number: remainingValues.invoice_number,
|
||||
vendorid: remainingValues.vendorid
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
upload.forEach((u) => {
|
||||
handleUpload(
|
||||
{ file: u.originFileObj },
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: values.jobid,
|
||||
billId: billId,
|
||||
tagsArray: null,
|
||||
callback: null
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
///////////////////////////
|
||||
setLoading(false);
|
||||
notification["success"]({
|
||||
message: t("bills.successes.created")
|
||||
});
|
||||
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
...formValues,
|
||||
vendorid:values.vendorid,
|
||||
billlines: [],
|
||||
});
|
||||
// form.resetFields();
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
setEnterAgain(false);
|
||||
};
|
||||
if (generateLabel) {
|
||||
GenerateDocument(
|
||||
{
|
||||
name: Templates.parts_invoice_label_single.key,
|
||||
variables: {
|
||||
id: billId
|
||||
}
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
const r = window.confirm(t("general.labels.cancel"));
|
||||
if (r === true) {
|
||||
toggleModalVisible();
|
||||
}
|
||||
};
|
||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||
|
||||
useEffect(() => {
|
||||
if (enterAgain) form.submit();
|
||||
}, [enterAgain, form]);
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
billid: billId,
|
||||
operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
|
||||
type: "billposted"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (billEnterModal.open) {
|
||||
form.setFieldsValue(formValues);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [billEnterModal.open, form, formValues]);
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
...formValues,
|
||||
vendorid: values.vendorid,
|
||||
billlines: []
|
||||
});
|
||||
// form.resetFields();
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
setEnterAgain(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("bills.labels.new")}
|
||||
width={"98%"}
|
||||
open={billEnterModal.open}
|
||||
okText={t("general.actions.save")}
|
||||
keyboard="false"
|
||||
onOk={() => form.submit()}
|
||||
onCancel={handleCancel}
|
||||
afterClose={() => {
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
}}
|
||||
footer={
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={generateLabel}
|
||||
onChange={(e) => setGenerateLabel(e.target.checked)}
|
||||
>
|
||||
{t("bills.labels.generatepartslabel")}
|
||||
</Checkbox>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<Button loading={loading} onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setEnterAgain(true);
|
||||
}}
|
||||
>
|
||||
{t("general.actions.saveandnew")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinishFailed={() => {
|
||||
setEnterAgain(false);
|
||||
}}
|
||||
const handleCancel = () => {
|
||||
const r = window.confirm(t("general.labels.cancel"));
|
||||
if (r === true) {
|
||||
toggleModalVisible();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (enterAgain) form.submit();
|
||||
}, [enterAgain, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (billEnterModal.open) {
|
||||
form.setFieldsValue(formValues);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [billEnterModal.open, form, formValues]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("bills.labels.new")}
|
||||
width={"98%"}
|
||||
open={billEnterModal.open}
|
||||
okText={t("general.actions.save")}
|
||||
keyboard="false"
|
||||
onOk={() => form.submit()}
|
||||
onCancel={handleCancel}
|
||||
afterClose={() => {
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
}}
|
||||
footer={
|
||||
<Space>
|
||||
<Checkbox checked={generateLabel} onChange={(e) => setGenerateLabel(e.target.checked)}>
|
||||
{t("bills.labels.generatepartslabel")}
|
||||
</Checkbox>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<Button loading={loading} onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setEnterAgain(true);
|
||||
}}
|
||||
>
|
||||
<BillFormContainer
|
||||
form={form}
|
||||
disableInvNumber={billEnterModal.context.disableInvNumber}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
{t("general.actions.saveandnew")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinishFailed={() => {
|
||||
setEnterAgain(false);
|
||||
}}
|
||||
>
|
||||
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillEnterModalContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);
|
||||
|
||||
@@ -1,136 +1,114 @@
|
||||
import {Form, Input, Table} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Form, Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
|
||||
|
||||
export default function BillFormLinesExtended({
|
||||
lineData,
|
||||
discount,
|
||||
form,
|
||||
responsibilityCenters,
|
||||
disabled,
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
const {t} = useTranslation();
|
||||
const columns = [
|
||||
export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc)
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
width: "10%",
|
||||
filters: [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
text: t("jobs.labels.partsfilter"),
|
||||
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
width: "10%",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
text: t("joblines.fields.part_types.PAN"),
|
||||
value: ["PAN", "PAP"]
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
width: "10%",
|
||||
filters: [
|
||||
{
|
||||
text: t("jobs.labels.partsfilter"),
|
||||
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAN"),
|
||||
value: ["PAN", "PAP"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAL"),
|
||||
value: ["PAL"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAA"),
|
||||
value: ["PAA"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAS"),
|
||||
value: ["PAS", "PASL"],
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) =>
|
||||
record.part_type
|
||||
? t(`joblines.fields.part_types.${record.part_type}`)
|
||||
: null,
|
||||
text: t("joblines.fields.part_types.PAL"),
|
||||
value: ["PAL"]
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAA"),
|
||||
value: ["PAA"]
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAS"),
|
||||
value: ["PAS", "PASL"]
|
||||
}
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
width: "10%",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
shouldCellUpdate: false,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||
? record.prt_dsmk_m
|
||||
: record.act_price}
|
||||
</CurrencyFormatter>
|
||||
{record.part_qty ? `(x ${record.part_qty})` : null}
|
||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||
<span
|
||||
style={{marginLeft: ".2rem"}}
|
||||
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.posting"),
|
||||
dataIndex: "posting",
|
||||
key: "posting",
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
width: "10%",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
shouldCellUpdate: false,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
|
||||
</CurrencyFormatter>
|
||||
{record.part_qty ? `(x ${record.part_qty})` : null}
|
||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||
<span style={{ marginLeft: ".2rem" }}>{`(${record.prt_dsmk_p}%)`}</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.posting"),
|
||||
dataIndex: "posting",
|
||||
key: "posting",
|
||||
|
||||
render: (text, record, index) => (
|
||||
<Form.Item noStyle name={["billlineskeys", record.id]}>
|
||||
<BillFormItemsExtendedFormItem
|
||||
form={form}
|
||||
record={record}
|
||||
index={index}
|
||||
responsibilityCenters={responsibilityCenters}
|
||||
discount={discount}
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const data =
|
||||
search === ""
|
||||
? lineData
|
||||
: lineData.filter(
|
||||
(l) =>
|
||||
(l.line_desc &&
|
||||
l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(l.oem_partno &&
|
||||
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(l.act_price &&
|
||||
l.act_price.toString().startsWith(search.toString()))
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item noStyle name="billlineskeys">
|
||||
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
|
||||
<Input onChange={(e) => setSearch(e.target.value)} allowClear/>
|
||||
<Table
|
||||
pagination={false}
|
||||
size="small"
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
/>
|
||||
render: (text, record, index) => (
|
||||
<Form.Item noStyle name={["billlineskeys", record.id]}>
|
||||
<BillFormItemsExtendedFormItem
|
||||
form={form}
|
||||
record={record}
|
||||
index={index}
|
||||
responsibilityCenters={responsibilityCenters}
|
||||
discount={discount}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const data =
|
||||
search === ""
|
||||
? lineData
|
||||
: lineData.filter(
|
||||
(l) =>
|
||||
(l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(l.act_price && l.act_price.toString().startsWith(search.toString()))
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Item noStyle name="billlineskeys">
|
||||
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
|
||||
<Input onChange={(e) => setSearch(e.target.value)} allowClear />
|
||||
<Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,284 +1,216 @@
|
||||
import React from "react";
|
||||
import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons";
|
||||
import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillFormItemsExtendedFormItem);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
|
||||
|
||||
export function BillFormItemsExtendedFormItem({
|
||||
value,
|
||||
bodyshop,
|
||||
form,
|
||||
record,
|
||||
index,
|
||||
disabled,
|
||||
responsibilityCenters,
|
||||
discount,
|
||||
}) {
|
||||
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
||||
|
||||
const {t} = useTranslation();
|
||||
if (!value)
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const values = form.getFieldsValue("billlineskeys");
|
||||
|
||||
form.setFieldsValue({
|
||||
...values,
|
||||
billlineskeys: {
|
||||
...(values.billlineskeys || {}),
|
||||
[record.id]: {
|
||||
joblineid: record.id,
|
||||
line_desc: record.line_desc,
|
||||
quantity: record.part_qty || 1,
|
||||
actual_price: record.act_price,
|
||||
cost_center: record.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? record.part_type
|
||||
: responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[record.part_type] ||
|
||||
null)
|
||||
: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PlusCircleFilled/>
|
||||
</Button>
|
||||
);
|
||||
value,
|
||||
bodyshop,
|
||||
form,
|
||||
record,
|
||||
index,
|
||||
disabled,
|
||||
responsibilityCenters,
|
||||
discount
|
||||
}) {
|
||||
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
||||
|
||||
const { t } = useTranslation();
|
||||
if (!value)
|
||||
return (
|
||||
<Space wrap>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.line_desc")}
|
||||
name={["billlineskeys", record.id, "line_desc"]}
|
||||
>
|
||||
<Input disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.quantity")}
|
||||
name={["billlineskeys", record.id, "quantity"]}
|
||||
>
|
||||
<InputNumber precision={0} min={0} disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.actual_price")}
|
||||
name={["billlineskeys", record.id, "actual_price"]}
|
||||
>
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
onBlur={(e) => {
|
||||
const {billlineskeys} = form.getFieldsValue("billlineskeys");
|
||||
form.setFieldsValue({
|
||||
billlineskeys: {
|
||||
...billlineskeys,
|
||||
[record.id]: {
|
||||
...billlineskeys[billlineskeys],
|
||||
actual_cost: !!billlineskeys[billlineskeys].actual_cost
|
||||
? billlineskeys[billlineskeys].actual_cost
|
||||
: Math.round(
|
||||
(parseFloat(e.target.value) * (1 - discount) +
|
||||
Number.EPSILON) *
|
||||
100
|
||||
) / 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.actual_cost")}
|
||||
name={["billlineskeys", record.id, "actual_cost"]}
|
||||
>
|
||||
<CurrencyInput min={0} disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const line = value;
|
||||
if (!!!line) return null;
|
||||
const lineDiscount = (
|
||||
1 -
|
||||
Math.round((line.actual_cost / line.actual_price) * 100) / 100
|
||||
).toPrecision(2);
|
||||
<Button
|
||||
onClick={() => {
|
||||
const values = form.getFieldsValue("billlineskeys");
|
||||
|
||||
if (lineDiscount - discount === 0) return <div/>;
|
||||
return <WarningOutlined style={{color: "red"}}/>;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.cost_center")}
|
||||
name={["billlineskeys", record.id, "cost_center"]}
|
||||
>
|
||||
<Select showSearch style={{minWidth: "3rem"}} disabled={disabled}>
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => (
|
||||
<Select.Option key={item.name}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.location")}
|
||||
name={["billlineskeys", record.id, "location"]}
|
||||
>
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.deductedfromlbr")}
|
||||
name={["billlineskeys", record.id, "deductedfromlbr"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate style={{display: "inline-block"}}>
|
||||
{() => {
|
||||
if (
|
||||
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
|
||||
.deductedfromlbr
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={[
|
||||
"billlineskeys",
|
||||
record.id,
|
||||
"lbr_adjustment",
|
||||
"mod_lbr_ty",
|
||||
]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">
|
||||
{t("joblines.fields.lbr_types.LAA")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAB">
|
||||
{t("joblines.fields.lbr_types.LAB")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAD">
|
||||
{t("joblines.fields.lbr_types.LAD")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAE">
|
||||
{t("joblines.fields.lbr_types.LAE")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAF">
|
||||
{t("joblines.fields.lbr_types.LAF")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAG">
|
||||
{t("joblines.fields.lbr_types.LAG")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAM">
|
||||
{t("joblines.fields.lbr_types.LAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAR">
|
||||
{t("joblines.fields.lbr_types.LAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAS">
|
||||
{t("joblines.fields.lbr_types.LAS")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAU">
|
||||
{t("joblines.fields.lbr_types.LAU")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA1">
|
||||
{t("joblines.fields.lbr_types.LA1")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA2">
|
||||
{t("joblines.fields.lbr_types.LA2")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA3">
|
||||
{t("joblines.fields.lbr_types.LA3")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA4">
|
||||
{t("joblines.fields.lbr_types.LA4")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.labels.adjustmentrate")}
|
||||
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
|
||||
initialValue={bodyshop.default_adjustment_rate}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={2} min={0.01}/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("billlines.fields.federal_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.state_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.local_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled}/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
const values = form.getFieldsValue("billlineskeys");
|
||||
|
||||
form.setFieldsValue({
|
||||
...values,
|
||||
billlineskeys: {
|
||||
...(values.billlineskeys || {}),
|
||||
[record.id]: null,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MinusCircleFilled/>
|
||||
</Button>
|
||||
</Space>
|
||||
form.setFieldsValue({
|
||||
...values,
|
||||
billlineskeys: {
|
||||
...(values.billlineskeys || {}),
|
||||
[record.id]: {
|
||||
joblineid: record.id,
|
||||
line_desc: record.line_desc,
|
||||
quantity: record.part_qty || 1,
|
||||
actual_price: record.act_price,
|
||||
cost_center: record.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? record.part_type
|
||||
: responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
|
||||
: null
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PlusCircleFilled />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Form.Item label={t("billlines.fields.line_desc")} name={["billlineskeys", record.id, "line_desc"]}>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.quantity")} name={["billlineskeys", record.id, "quantity"]}>
|
||||
<InputNumber precision={0} min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.actual_price")} name={["billlineskeys", record.id, "actual_price"]}>
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
onBlur={(e) => {
|
||||
const { billlineskeys } = form.getFieldsValue("billlineskeys");
|
||||
form.setFieldsValue({
|
||||
billlineskeys: {
|
||||
...billlineskeys,
|
||||
[record.id]: {
|
||||
...billlineskeys[billlineskeys],
|
||||
actual_cost: !!billlineskeys[billlineskeys].actual_cost
|
||||
? billlineskeys[billlineskeys].actual_cost
|
||||
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.actual_cost")} name={["billlineskeys", record.id, "actual_cost"]}>
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const line = value;
|
||||
if (!!!line) return null;
|
||||
const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
|
||||
|
||||
if (lineDiscount - discount === 0) return <div />;
|
||||
return <WarningOutlined style={{ color: "red" }} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.deductedfromlbr")}
|
||||
name={["billlineskeys", record.id, "deductedfromlbr"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
|
||||
{() => {
|
||||
if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr)
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
|
||||
>
|
||||
<Select allowClear>
|
||||
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
||||
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
||||
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
||||
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
||||
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
||||
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
||||
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
||||
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
||||
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
||||
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
||||
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
||||
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
||||
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
||||
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.labels.adjustmentrate")}
|
||||
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
|
||||
initialValue={bodyshop.default_adjustment_rate}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={2} min={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("billlines.fields.federal_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.state_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("billlines.fields.local_tax_applicable")}
|
||||
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch disabled={disabled} />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
const values = form.getFieldsValue("billlineskeys");
|
||||
|
||||
form.setFieldsValue({
|
||||
...values,
|
||||
billlineskeys: {
|
||||
...(values.billlineskeys || {}),
|
||||
[record.id]: null
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MinusCircleFilled />
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import Icon, { UploadOutlined } from '@ant-design/icons';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useSplitTreatments } from '@splitsoftware/splitio-react';
|
||||
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdOpenInNew } from 'react-icons/md';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import dayjs from '../../utils/day';
|
||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
||||
import AlertComponent from '../alert/alert.component';
|
||||
import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component';
|
||||
import FormDatePicker from '../form-date-picker/form-date-picker.component';
|
||||
import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component';
|
||||
import CurrencyInput from '../form-items-formatted/currency-form-item.component';
|
||||
import JobSearchSelect from '../job-search-select/job-search-select.component';
|
||||
import LayoutFormRow from '../layout-form-row/layout-form-row.component';
|
||||
import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component';
|
||||
import BillFormLines from './bill-form.lines.component';
|
||||
import { CalculateBillTotal } from './bill-form.totals.utility';
|
||||
import Icon, { UploadOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdOpenInNew } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import BillFormLines from "./bill-form.lines.component";
|
||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
@@ -41,18 +41,18 @@ export function BillFormComponent({
|
||||
job,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
preferredMake,
|
||||
preferredMake
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
const [discount, setDiscount] = useState(0);
|
||||
|
||||
const {
|
||||
treatments: { Extended_Bill_Posting, ClosingPeriod },
|
||||
treatments: { Extended_Bill_Posting, ClosingPeriod }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ['Extended_Bill_Posting', 'ClosingPeriod'],
|
||||
splitKey: bodyshop.imexshopid,
|
||||
names: ["Extended_Bill_Posting", "ClosingPeriod"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const handleVendorSelect = (props, opt) => {
|
||||
@@ -62,16 +62,16 @@ export function BillFormComponent({
|
||||
!billEdit &&
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue('jobid'),
|
||||
vendorId: opt.value,
|
||||
},
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: opt.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
||||
// Early gate
|
||||
if (!checked) return;
|
||||
const values = form.getFieldsValue('billlines');
|
||||
const values = form.getFieldsValue("billlines");
|
||||
// Gate bill lines
|
||||
if (!values?.billlines?.length) return;
|
||||
|
||||
@@ -83,26 +83,26 @@ export function BillFormComponent({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (job) form.validateFields(['is_credit_memo']);
|
||||
if (job) form.validateFields(["is_credit_memo"]);
|
||||
}, [job, form]);
|
||||
|
||||
useEffect(() => {
|
||||
const vendorId = form.getFieldValue('vendorid');
|
||||
const vendorId = form.getFieldValue("vendorid");
|
||||
if (vendorId && vendorAutoCompleteOptions) {
|
||||
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
|
||||
if (matchingVendors.length === 1) {
|
||||
setDiscount(matchingVendors[0].discount);
|
||||
}
|
||||
}
|
||||
const jobId = form.getFieldValue('jobid');
|
||||
const jobId = form.getFieldValue("jobid");
|
||||
if (jobId) {
|
||||
loadLines({ variables: { id: jobId } });
|
||||
if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) {
|
||||
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: jobId,
|
||||
vendorId: vendorId,
|
||||
},
|
||||
vendorId: vendorId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -118,24 +118,24 @@ export function BillFormComponent({
|
||||
setDiscount,
|
||||
vendorAutoCompleteOptions,
|
||||
loadLines,
|
||||
bodyshop.inhousevendorid,
|
||||
bodyshop.inhousevendorid
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form} />
|
||||
<Form.Item style={{ display: 'none' }} name="isinhouse" valuePropName="checked">
|
||||
<Form.Item style={{ display: "none" }} name="isinhouse" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t('bills.fields.ro_number')}
|
||||
label={t("bills.fields.ro_number")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
}
|
||||
]}
|
||||
>
|
||||
<JobSearchSelect
|
||||
@@ -143,20 +143,14 @@ export function BillFormComponent({
|
||||
convertedOnly
|
||||
notExported={false}
|
||||
onBlur={() => {
|
||||
if (
|
||||
form.getFieldValue('jobid') !== null &&
|
||||
form.getFieldValue('jobid') !== undefined
|
||||
) {
|
||||
loadLines({ variables: { id: form.getFieldValue('jobid') } });
|
||||
if (
|
||||
form.getFieldValue('vendorid') !== null &&
|
||||
form.getFieldValue('vendorid') !== undefined
|
||||
) {
|
||||
if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue('jobid'),
|
||||
vendorId: form.getFieldValue('vendorid'),
|
||||
},
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: form.getFieldValue("vendorid")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -164,22 +158,22 @@ export function BillFormComponent({
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('bills.fields.vendor')}
|
||||
label={t("bills.fields.vendor")}
|
||||
name="vendorid"
|
||||
// style={{ display: billEdit ? "none" : null }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) {
|
||||
return Promise.reject(t('bills.validation.manualinhouse'));
|
||||
if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) {
|
||||
return Promise.reject(t("bills.validation.manualinhouse"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
@@ -199,12 +193,8 @@ export function BillFormComponent({
|
||||
type="warning"
|
||||
message={
|
||||
<Space>
|
||||
{t('bills.labels.iouexists')}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
to={`/manage/jobs/${iou.id}?tab=repairdata`}
|
||||
>
|
||||
{t("bills.labels.iouexists")}
|
||||
<Link target="_blank" rel="noopener noreferrer" to={`/manage/jobs/${iou.id}?tab=repairdata`}>
|
||||
<Space>
|
||||
{iou.ro_number}
|
||||
<Icon component={MdOpenInNew} />
|
||||
@@ -216,89 +206,85 @@ export function BillFormComponent({
|
||||
))}
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t('bills.fields.invoice_number')}
|
||||
label={t("bills.fields.invoice_number")}
|
||||
name="invoice_number"
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
const vendorid = getFieldValue('vendorid');
|
||||
const vendorid = getFieldValue("vendorid");
|
||||
if (vendorid && value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_BILL_INVOICE_NUMBER,
|
||||
variables: {
|
||||
invoice_number: value,
|
||||
vendorid: vendorid,
|
||||
},
|
||||
vendorid: vendorid
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.bills_aggregate.nodes.length === 1 &&
|
||||
response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id')
|
||||
response.data.bills_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t('bills.validation.unique_invoice_number'));
|
||||
return Promise.reject(t("bills.validation.unique_invoice_number"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('bills.fields.date')}
|
||||
label={t("bills.fields.date")}
|
||||
name="date"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) {
|
||||
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
|
||||
if (
|
||||
dayjs(value)
|
||||
.startOf('day')
|
||||
.isSameOrAfter(
|
||||
dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
|
||||
) &&
|
||||
.startOf("day")
|
||||
.isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
|
||||
dayjs(value)
|
||||
.startOf('day')
|
||||
.isSameOrBefore(
|
||||
dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
|
||||
)
|
||||
.startOf("day")
|
||||
.isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day"))
|
||||
) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(t('bills.validation.closingperiod'));
|
||||
return Promise.reject(t("bills.validation.closingperiod"));
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormDatePicker disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('bills.fields.is_credit_memo')}
|
||||
label={t("bills.fields.is_credit_memo")}
|
||||
name="is_credit_memo"
|
||||
valuePropName="checked"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) {
|
||||
if (value === true && getFieldValue("jobid") && getFieldValue("vendorid")) {
|
||||
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
||||
// loadOutstandingReturns({
|
||||
// variables: {
|
||||
@@ -316,31 +302,31 @@ export function BillFormComponent({
|
||||
job.status === bodyshop.md_ro_statuses.default_void) &&
|
||||
(value === false || !value)
|
||||
) {
|
||||
return Promise.reject(t('bills.labels.onlycmforinvoiced'));
|
||||
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('bills.fields.total')}
|
||||
label={t("bills.fields.total")}
|
||||
name="total"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
{!billEdit && (
|
||||
<Form.Item label={t('bills.fields.allpartslocation')} name="location">
|
||||
<Select style={{ width: '10rem' }} disabled={disabled} allowClear>
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
@@ -353,40 +339,36 @@ export function BillFormComponent({
|
||||
<LayoutFormRow>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item span={3} label={t('bills.fields.federal_tax_rate')} name="federal_tax_rate">
|
||||
<Form.Item span={3} label={t("bills.fields.federal_tax_rate")} name="federal_tax_rate">
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
),
|
||||
)
|
||||
})}
|
||||
<Form.Item span={3} label={t('bills.fields.state_tax_rate')} name="state_tax_rate">
|
||||
<Form.Item span={3} label={t("bills.fields.state_tax_rate")} name="state_tax_rate">
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<>
|
||||
<Form.Item span={3} label={t('bills.fields.local_tax_rate')} name="local_tax_rate">
|
||||
<Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
<Form.Item
|
||||
span={2}
|
||||
label={t('bills.labels.federal_tax_exempt')}
|
||||
name="federal_tax_exempt"
|
||||
>
|
||||
<Form.Item span={2} label={t("bills.labels.federal_tax_exempt")} name="federal_tax_exempt">
|
||||
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
)
|
||||
})}
|
||||
<Form.Item shouldUpdate span={13}>
|
||||
{() => {
|
||||
const values = form.getFieldsValue([
|
||||
'billlines',
|
||||
'total',
|
||||
'federal_tax_rate',
|
||||
'state_tax_rate',
|
||||
'local_tax_rate',
|
||||
"billlines",
|
||||
"total",
|
||||
"federal_tax_rate",
|
||||
"state_tax_rate",
|
||||
"local_tax_rate"
|
||||
]);
|
||||
let totals;
|
||||
if (!!values.total && !!values.billlines && values.billlines.length > 0)
|
||||
@@ -394,56 +376,48 @@ export function BillFormComponent({
|
||||
if (!!totals)
|
||||
return (
|
||||
<div align="right">
|
||||
<Space size='large' wrap>
|
||||
<Statistic
|
||||
title={t('bills.labels.subtotal')}
|
||||
value={totals.subtotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Space size="large" wrap>
|
||||
<Statistic title={t("bills.labels.subtotal")} value={totals.subtotal.toFormat()} precision={2} />
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Statistic
|
||||
title={t('bills.labels.federal_tax')}
|
||||
title={t("bills.labels.federal_tax")}
|
||||
value={totals.federalTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
),
|
||||
)
|
||||
})}
|
||||
<Statistic
|
||||
title={t('bills.labels.state_tax')}
|
||||
value={totals.stateTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic title={t("bills.labels.state_tax")} value={totals.stateTax.toFormat()} precision={2} />
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Statistic
|
||||
title={t('bills.labels.local_tax')}
|
||||
title={t("bills.labels.local_tax")}
|
||||
value={totals.localTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
),
|
||||
)
|
||||
})}
|
||||
<Statistic
|
||||
title={t('bills.labels.entered_total')}
|
||||
title={t("bills.labels.entered_total")}
|
||||
value={totals.enteredTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t('bills.labels.bill_total')}
|
||||
title={t("bills.labels.bill_total")}
|
||||
value={totals.invoiceTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t('bills.labels.discrepancy')}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color: totals.discrepancy.getAmount() === 0 ? 'green' : 'red',
|
||||
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
|
||||
}}
|
||||
value={totals.discrepancy.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
</Space>
|
||||
{form.getFieldValue('is_credit_memo') ? (
|
||||
<AlertComponent type="warning" message={t('bills.labels.enteringcreditmemo')} />
|
||||
{form.getFieldValue("is_credit_memo") ? (
|
||||
<AlertComponent type="warning" message={t("bills.labels.enteringcreditmemo")} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -451,9 +425,9 @@ export function BillFormComponent({
|
||||
}}
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Divider orientation="left">{t('bills.labels.bill_lines')}</Divider>
|
||||
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
||||
|
||||
{Extended_Bill_Posting.treatment === 'on' ? (
|
||||
{Extended_Bill_Posting.treatment === "on" ? (
|
||||
<BillFormLinesExtended
|
||||
lineData={lineData}
|
||||
discount={discount}
|
||||
@@ -471,13 +445,13 @@ export function BillFormComponent({
|
||||
billEdit={billEdit}
|
||||
/>
|
||||
)}
|
||||
<Divider orientation="left" style={{ display: billEdit ? 'none' : null }}>
|
||||
{t('documents.labels.upload')}
|
||||
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
|
||||
{t("documents.labels.upload")}
|
||||
</Divider>
|
||||
<Form.Item
|
||||
name="upload"
|
||||
label="Upload"
|
||||
style={{ display: billEdit ? 'none' : null }}
|
||||
style={{ display: billEdit ? "none" : null }}
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={(e) => {
|
||||
if (Array.isArray(e)) {
|
||||
|
||||
@@ -1,83 +1,67 @@
|
||||
import {useLazyQuery, useQuery} from "@apollo/client";
|
||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries";
|
||||
import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries";
|
||||
import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries";
|
||||
import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
||||
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||
import BillFormComponent from "./bill-form.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function BillFormContainer({
|
||||
bodyshop,
|
||||
form,
|
||||
billEdit,
|
||||
disabled,
|
||||
disableInvNumber,
|
||||
}) {
|
||||
const {treatments: {Simple_Inventory}} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Simple_Inventory"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid,
|
||||
});
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
||||
const {
|
||||
treatments: { Simple_Inventory }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Simple_Inventory"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const {data: VendorAutoCompleteData} = useQuery(
|
||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
|
||||
);
|
||||
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const [loadLines, {data: lineData}] = useLazyQuery(
|
||||
GET_JOB_LINES_TO_ENTER_BILL
|
||||
);
|
||||
const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
|
||||
|
||||
const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] =
|
||||
useLazyQuery(QUERY_UNRECEIVED_LINES);
|
||||
const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
|
||||
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
||||
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
|
||||
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BillFormComponent
|
||||
disabled={disabled}
|
||||
form={form}
|
||||
billEdit={billEdit}
|
||||
vendorAutoCompleteOptions={
|
||||
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
||||
}
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||
/>
|
||||
{!billEdit && (
|
||||
<BillCmdReturnsTableComponent
|
||||
form={form}
|
||||
returnLoading={returnLoading}
|
||||
returnData={returnData}
|
||||
/>
|
||||
)}
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<BillInventoryTable
|
||||
form={form}
|
||||
inventoryLoading={inventoryLoading}
|
||||
inventoryData={billEdit ? [] : inventoryData}
|
||||
billEdit={billEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<BillFormComponent
|
||||
disabled={disabled}
|
||||
form={form}
|
||||
billEdit={billEdit}
|
||||
vendorAutoCompleteOptions={VendorAutoCompleteData && VendorAutoCompleteData.vendors}
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||
/>
|
||||
{!billEdit && <BillCmdReturnsTableComponent form={form} returnLoading={returnLoading} returnData={returnData} />}
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<BillInventoryTable
|
||||
form={form}
|
||||
inventoryLoading={inventoryLoading}
|
||||
inventoryData={billEdit ? [] : inventoryData}
|
||||
billEdit={billEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(BillFormContainer);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +1,42 @@
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
export const CalculateBillTotal = (invoice) => {
|
||||
const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} =
|
||||
invoice;
|
||||
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
|
||||
|
||||
//TODO Determine why this recalculates so many times.
|
||||
let subtotal = Dinero({amount: 0});
|
||||
let federalTax = Dinero({amount: 0});
|
||||
let stateTax = Dinero({amount: 0});
|
||||
let localTax = Dinero({amount: 0});
|
||||
//TODO Determine why this recalculates so many times.
|
||||
let subtotal = Dinero({ amount: 0 });
|
||||
let federalTax = Dinero({ amount: 0 });
|
||||
let stateTax = Dinero({ amount: 0 });
|
||||
let localTax = Dinero({ amount: 0 });
|
||||
|
||||
if (!!!billlines) return null;
|
||||
if (!!!billlines) return null;
|
||||
|
||||
billlines.forEach((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({
|
||||
amount: Math.round((i.actual_cost || 0) * 100),
|
||||
}).multiply(i.quantity || 1);
|
||||
billlines.forEach((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({
|
||||
amount: Math.round((i.actual_cost || 0) * 100)
|
||||
}).multiply(i.quantity || 1);
|
||||
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
if (i.applicable_taxes?.federal) {
|
||||
federalTax = federalTax.add(
|
||||
itemTotal.percentage(federal_tax_rate || 0)
|
||||
);
|
||||
}
|
||||
if (i.applicable_taxes?.state)
|
||||
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
|
||||
if (i.applicable_taxes?.local)
|
||||
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
|
||||
}
|
||||
});
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
if (i.applicable_taxes?.federal) {
|
||||
federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0));
|
||||
}
|
||||
if (i.applicable_taxes?.state) stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
|
||||
if (i.applicable_taxes?.local) localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
|
||||
}
|
||||
});
|
||||
|
||||
const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)});
|
||||
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
|
||||
const discrepancy = enteredTotal.subtract(invoiceTotal);
|
||||
const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) });
|
||||
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
|
||||
const discrepancy = enteredTotal.subtract(invoiceTotal);
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
federalTax,
|
||||
stateTax,
|
||||
localTax,
|
||||
enteredTotal,
|
||||
invoiceTotal,
|
||||
discrepancy,
|
||||
};
|
||||
return {
|
||||
subtotal,
|
||||
federalTax,
|
||||
stateTax,
|
||||
localTax,
|
||||
enteredTotal,
|
||||
invoiceTotal,
|
||||
discrepancy
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,173 +1,153 @@
|
||||
import {Checkbox, Form, Skeleton, Typography} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||
import "./bill-inventory-table.styles.scss";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
billEnterModal: selectBillEnterModal,
|
||||
bodyshop: selectBodyshop,
|
||||
billEnterModal: selectBillEnterModal
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
|
||||
|
||||
export function BillInventoryTable({
|
||||
billEnterModal,
|
||||
bodyshop,
|
||||
form,
|
||||
billEdit,
|
||||
inventoryLoading,
|
||||
inventoryData,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (inventoryData && inventoryData.inventory) {
|
||||
form.setFieldsValue({
|
||||
inventory: billEnterModal.context.consumeinventoryid
|
||||
? inventoryData.inventory.map((i) => {
|
||||
if (i.id === billEnterModal.context.consumeinventoryid)
|
||||
i.consumefrominventory = true;
|
||||
return i;
|
||||
})
|
||||
: inventoryData.inventory,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (inventoryData && inventoryData.inventory) {
|
||||
form.setFieldsValue({
|
||||
inventory: billEnterModal.context.consumeinventoryid
|
||||
? inventoryData.inventory.map((i) => {
|
||||
if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
|
||||
return i;
|
||||
})
|
||||
: inventoryData.inventory
|
||||
});
|
||||
}
|
||||
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
||||
|
||||
return (
|
||||
<Form.Item shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid} noStyle>
|
||||
{() => {
|
||||
const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||
|
||||
if (!is_inhouse || billEdit) {
|
||||
return null;
|
||||
}
|
||||
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
|
||||
noStyle
|
||||
>
|
||||
{() => {
|
||||
const is_inhouse =
|
||||
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||
if (inventoryLoading) return <Skeleton />;
|
||||
|
||||
if (!is_inhouse || billEdit) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Form.List name="inventory">
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>{t("inventory.labels.inventory")}</Typography.Title>
|
||||
<table className="bill-inventory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("billlines.fields.line_desc")}</th>
|
||||
<th>{t("vendors.fields.name")}</th>
|
||||
<th>{t("billlines.fields.quantity")}</th>
|
||||
<th>{t("billlines.fields.actual_price")}</th>
|
||||
<th>{t("billlines.fields.actual_cost")}</th>
|
||||
<th>{t("inventory.fields.comment")}</th>
|
||||
<th>{t("inventory.actions.consumefrominventory")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
if (inventoryLoading) return <Skeleton/>;
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}part_type`}
|
||||
name={[field.name, "billline", "bill", "vendor", "name"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}comment`}
|
||||
name={[field.name, "comment"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
return (
|
||||
<Form.List name="inventory">
|
||||
{(fields, {add, remove, move}) => {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("inventory.labels.inventory")}
|
||||
</Typography.Title>
|
||||
<table className="bill-inventory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("billlines.fields.line_desc")}</th>
|
||||
<th>{t("vendors.fields.name")}</th>
|
||||
<th>{t("billlines.fields.quantity")}</th>
|
||||
<th>{t("billlines.fields.actual_price")}</th>
|
||||
<th>{t("billlines.fields.actual_cost")}</th>
|
||||
<th>{t("inventory.fields.comment")}</th>
|
||||
<th>{t("inventory.actions.consumefrominventory")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.line_desc")}
|
||||
key={`${index}line_desc`}
|
||||
name={[field.name, "line_desc"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}part_type`}
|
||||
name={[
|
||||
field.name,
|
||||
"billline",
|
||||
"bill",
|
||||
"vendor",
|
||||
"name",
|
||||
]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency"/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}comment`}
|
||||
name={[field.name, "comment"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}consumefrominventory`}
|
||||
name={[field.name, "consumefrominventory"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox/>
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
);
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
//label={t("joblines.fields.mod_lb_hrs")}
|
||||
key={`${index}consumefrominventory`}
|
||||
name={[field.name, "consumefrominventory"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
</Form.List>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +1,73 @@
|
||||
import {Select} from "antd";
|
||||
import React, {forwardRef} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import InstanceRenderMgr from '../../utils/instanceRenderMgr';
|
||||
import { Select } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||
|
||||
//To be used as a form element only.
|
||||
const {Option} = Select;
|
||||
const BillLineSearchSelect = (
|
||||
{options, disabled, allowRemoved, ...restProps},
|
||||
ref
|
||||
) => {
|
||||
const {t} = useTranslation();
|
||||
const { Option } = Select;
|
||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
showSearch
|
||||
popupMatchSelectWidth={false}
|
||||
optionLabelProp={"name"}
|
||||
// optionFilterProp="line_desc"
|
||||
filterOption={(inputValue, option) => {
|
||||
return (
|
||||
(option.line_desc &&
|
||||
option.line_desc
|
||||
.toLowerCase()
|
||||
.includes(inputValue.toLowerCase())) ||
|
||||
(option.oem_partno &&
|
||||
option.oem_partno
|
||||
.toLowerCase()
|
||||
.includes(inputValue.toLowerCase())) ||
|
||||
(option.alt_partno &&
|
||||
option.alt_partno
|
||||
.toLowerCase()
|
||||
.includes(inputValue.toLowerCase())) ||
|
||||
(option.act_price &&
|
||||
option.act_price.toString().startsWith(inputValue.toString()))
|
||||
);
|
||||
}}
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={allowRemoved ? false : item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
oem_partno={item.oem_partno}
|
||||
alt_partno={item.alt_partno}
|
||||
act_price={item.act_price}
|
||||
style={{
|
||||
...(item.removed ? {textDecoration: "line-through"} : {}),
|
||||
}}
|
||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
>
|
||||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
showSearch
|
||||
popupMatchSelectWidth={false}
|
||||
optionLabelProp={"name"}
|
||||
// optionFilterProp="line_desc"
|
||||
filterOption={(inputValue, option) => {
|
||||
return (
|
||||
(option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
(option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
(option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
(option.act_price && option.act_price.toString().startsWith(inputValue.toString()))
|
||||
);
|
||||
}}
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={allowRemoved ? false : item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
oem_partno={item.oem_partno}
|
||||
alt_partno={item.alt_partno}
|
||||
act_price={item.act_price}
|
||||
style={{
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
}}
|
||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
>
|
||||
<span>
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
</span>
|
||||
{
|
||||
InstanceRenderMgr
|
||||
(
|
||||
{
|
||||
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
|
||||
<span style={{float: "right", paddingleft: "1rem"}}>
|
||||
{`${item.mod_lb_hrs} units`}
|
||||
</span>
|
||||
)
|
||||
{InstanceRenderMgr({
|
||||
rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
||||
)
|
||||
})}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
<span style={{float: "right", paddingleft: "1rem"}}>
|
||||
{item.act_price
|
||||
? `$${item.act_price && item.act_price.toFixed(2)}`
|
||||
: ``}
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||
</span>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
);
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(BillLineSearchSelect);
|
||||
|
||||
@@ -1,97 +1,89 @@
|
||||
import {gql, useMutation} from "@apollo/client";
|
||||
import {Button, notification} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel,
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillMarkExportedButton);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
|
||||
|
||||
export function BillMarkExportedButton({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
authLevel,
|
||||
bill,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billId: uuid!) {
|
||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
|
||||
returning {
|
||||
id
|
||||
exported
|
||||
exported_at
|
||||
}
|
||||
}
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billId: uuid!) {
|
||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
|
||||
returning {
|
||||
id
|
||||
exported
|
||||
exported_at
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
const result = await updateBill({
|
||||
variables: {billId: bill.id},
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: [
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
billid: bill.id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.markexported"),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
//Get the owner details, populate it all back into the job.
|
||||
};
|
||||
|
||||
const hasAccess = HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
action: "bills:reexport",
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
const result = await updateBill({
|
||||
variables: { billId: bill.id }
|
||||
});
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
);
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: [
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
billid: bill.id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return <></>;
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.markexported")
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
//Get the owner details, populate it all back into the job.
|
||||
};
|
||||
|
||||
const hasAccess = HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
action: "bills:reexport"
|
||||
});
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import {Button, Space} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import { Button, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
|
||||
export default function BillPrintButton({billid}) {
|
||||
const {t} = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const Templates = TemplateList("job_special");
|
||||
export default function BillPrintButton({ billid }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const Templates = TemplateList("job_special");
|
||||
|
||||
const submitHandler = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: Templates.parts_invoice_label_single.key,
|
||||
variables: {
|
||||
id: billid,
|
||||
},
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Warning: Error generating a document.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const submitHandler = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: Templates.parts_invoice_label_single.key,
|
||||
variables: {
|
||||
id: billid
|
||||
}
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Warning: Error generating a document.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button loading={loading} onClick={submitHandler}>
|
||||
{t("bills.labels.printlabels")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button loading={loading} onClick={submitHandler}>
|
||||
{t("bills.labels.printlabels")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,72 @@
|
||||
import {gql, useMutation} from "@apollo/client";
|
||||
import {Button, notification} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
|
||||
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel,
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillMarkForReexportButton);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
|
||||
|
||||
export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
|
||||
const {t} = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billId: uuid!) {
|
||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
|
||||
returning {
|
||||
id
|
||||
exported
|
||||
exported_at
|
||||
}
|
||||
}
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billId: uuid!) {
|
||||
update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
|
||||
returning {
|
||||
id
|
||||
exported
|
||||
exported_at
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
const result = await updateBill({
|
||||
variables: {billId: bill.id},
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.reexport"),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
//Get the owner details, populate it all back into the job.
|
||||
};
|
||||
|
||||
const hasAccess = HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
action: "bills:reexport",
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
const result = await updateBill({
|
||||
variables: { billId: bill.id }
|
||||
});
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={!bill.exported}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
{t("bills.labels.markforreexport")}
|
||||
</Button>
|
||||
);
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.reexport")
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
//Get the owner details, populate it all back into the job.
|
||||
};
|
||||
|
||||
return <></>;
|
||||
const hasAccess = HasRbacAccess({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
action: "bills:reexport"
|
||||
});
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button loading={loading} disabled={!bill.exported} onClick={handleUpdate}>
|
||||
{t("bills.labels.markforreexport")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -1,151 +1,136 @@
|
||||
import {FileAddFilled} from "@ant-design/icons";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, notification, Tooltip} from "antd";
|
||||
import {t} from "i18next";
|
||||
import { FileAddFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Tooltip } from "antd";
|
||||
import { t } from "i18next";
|
||||
import dayjs from "./../../utils/day";
|
||||
import React, {useState} from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||
import queryString from "query-string";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BilllineAddInventory);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
|
||||
|
||||
export function BilllineAddInventory({
|
||||
currentUser,
|
||||
bodyshop,
|
||||
billline,
|
||||
disabled,
|
||||
jobid,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {billid} = queryString.parse(useLocation().search);
|
||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||
export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { billid } = queryString.parse(useLocation().search);
|
||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||
|
||||
const addToInventory = async () => {
|
||||
setLoading(true);
|
||||
const addToInventory = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Check to make sure there are no existing items already in the inventory.
|
||||
//Check to make sure there are no existing items already in the inventory.
|
||||
|
||||
const cm = {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
jobid: jobid,
|
||||
isinhouse: true,
|
||||
is_credit_memo: true,
|
||||
date: dayjs().format("YYYY-MM-DD"),
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||
total: 0,
|
||||
billlines: [
|
||||
{
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc,
|
||||
cost_center: billline.cost_center,
|
||||
deductedfromlbr: billline.deductedfromlbr,
|
||||
applicable_taxes: {
|
||||
local: billline.applicable_taxes.local,
|
||||
state: billline.applicable_taxes.state,
|
||||
federal: billline.applicable_taxes.federal,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||
|
||||
const insertResult = await insertInventoryLine({
|
||||
variables: {
|
||||
joblineId:
|
||||
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
|
||||
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||
inv: {
|
||||
shopid: bodyshop.id,
|
||||
billlineid: billline.id,
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc,
|
||||
},
|
||||
cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
|
||||
pol: {
|
||||
returnfrombill: billid,
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
deliver_by: dayjs().format("YYYY-MM-DD"),
|
||||
parts_order_lines: {
|
||||
data: [
|
||||
{
|
||||
line_desc: billline.line_desc,
|
||||
|
||||
act_price: billline.actual_price,
|
||||
cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
job_line_id:
|
||||
billline.joblineid === "noline" ? null : billline.joblineid,
|
||||
part_type: billline.jobline && billline.jobline.part_type,
|
||||
cm_received: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
order_date: "2022-06-01",
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobid,
|
||||
user_email: currentUser.email,
|
||||
return: true,
|
||||
status: "Ordered",
|
||||
},
|
||||
},
|
||||
refetchQueries: ["QUERY_BILL_BY_PK"],
|
||||
});
|
||||
|
||||
if (!insertResult.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
message: t("inventory.successes.inserted"),
|
||||
});
|
||||
} else {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("inventory.errors.inserting", {
|
||||
error: JSON.stringify(insertResult.errors),
|
||||
}),
|
||||
});
|
||||
const cm = {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
jobid: jobid,
|
||||
isinhouse: true,
|
||||
is_credit_memo: true,
|
||||
date: dayjs().format("YYYY-MM-DD"),
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||
total: 0,
|
||||
billlines: [
|
||||
{
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc,
|
||||
cost_center: billline.cost_center,
|
||||
deductedfromlbr: billline.deductedfromlbr,
|
||||
applicable_taxes: {
|
||||
local: billline.applicable_taxes.local,
|
||||
state: billline.applicable_taxes.state,
|
||||
federal: billline.applicable_taxes.federal
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={
|
||||
disabled || billline?.inventories?.length >= billline.quantity
|
||||
}
|
||||
onClick={addToInventory}
|
||||
>
|
||||
<FileAddFilled/>
|
||||
{billline?.inventories?.length > 0 && (
|
||||
<div>({billline?.inventories?.length} in inv)</div>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||
|
||||
const insertResult = await insertInventoryLine({
|
||||
variables: {
|
||||
joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
|
||||
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||
inv: {
|
||||
shopid: bodyshop.id,
|
||||
billlineid: billline.id,
|
||||
actual_price: billline.actual_price,
|
||||
actual_cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
line_desc: billline.line_desc
|
||||
},
|
||||
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
|
||||
pol: {
|
||||
returnfrombill: billid,
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
deliver_by: dayjs().format("YYYY-MM-DD"),
|
||||
parts_order_lines: {
|
||||
data: [
|
||||
{
|
||||
line_desc: billline.line_desc,
|
||||
|
||||
act_price: billline.actual_price,
|
||||
cost: billline.actual_cost,
|
||||
quantity: billline.quantity,
|
||||
job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
|
||||
part_type: billline.jobline && billline.jobline.part_type,
|
||||
cm_received: true
|
||||
}
|
||||
]
|
||||
},
|
||||
order_date: "2022-06-01",
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobid,
|
||||
user_email: currentUser.email,
|
||||
return: true,
|
||||
status: "Ordered"
|
||||
}
|
||||
},
|
||||
refetchQueries: ["QUERY_BILL_BY_PK"]
|
||||
});
|
||||
|
||||
if (!insertResult.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
message: t("inventory.successes.inserted")
|
||||
});
|
||||
} else {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("inventory.errors.inserting", {
|
||||
error: JSON.stringify(insertResult.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled || billline?.inventories?.length >= billline.quantity}
|
||||
onClick={addToInventory}
|
||||
>
|
||||
<FileAddFilled />
|
||||
{billline?.inventories?.length > 0 && <div>({billline?.inventories?.length} in inv)</div>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,236 +1,209 @@
|
||||
import {EditFilled, SyncOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Checkbox, Input, Space, Table} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectJobReadOnly} from "../../redux/application/application.selectors";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { 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 { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "billEnter"})),
|
||||
setReconciliationContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "reconciliation"})),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
|
||||
});
|
||||
|
||||
export function BillsListTableComponent({
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsOrderContext,
|
||||
setBillEnterContext,
|
||||
setReconciliationContext,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsOrderContext,
|
||||
setBillEnterContext,
|
||||
setReconciliationContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
// const search = queryString.parse(useLocation().search);
|
||||
// const selectedBill = search.billid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
// const search = queryString.parse(useLocation().search);
|
||||
// const selectedBill = search.billid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const Templates = TemplateList("bill");
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const { refetch } = billsQuery;
|
||||
const recordActions = (record, showView = false) => (
|
||||
const Templates = TemplateList("bill");
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const { refetch } = billsQuery;
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
|
||||
/>
|
||||
|
||||
{record.isinhouse && (
|
||||
<PrintWrapperComponent
|
||||
templateObject={{
|
||||
name: Templates.inhouse_invoice.key,
|
||||
variables: { id: record.id }
|
||||
}}
|
||||
messageObject={{ subject: Templates.inhouse_invoice.subject }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.is_credit_memo} />
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.exported"),
|
||||
dataIndex: "exported",
|
||||
key: "exported",
|
||||
sorter: (a, b) => a.exported - b.exported,
|
||||
sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.exported} />
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => recordActions(record, true)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const filteredBills = bills
|
||||
? searchText === ""
|
||||
? bills
|
||||
: bills.filter(
|
||||
(b) =>
|
||||
(b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("bills.labels.bills")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={
|
||||
record.is_credit_memo ||
|
||||
record.vendorid === bodyshop.inhousevendorid ||
|
||||
jobRO
|
||||
}
|
||||
/>
|
||||
|
||||
{record.isinhouse && (
|
||||
<PrintWrapperComponent
|
||||
templateObject={{
|
||||
name: Templates.inhouse_invoice.key,
|
||||
variables: {id: record.id},
|
||||
}}
|
||||
messageObject={{subject: Templates.inhouse_invoice.subject}}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "invoice_number" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.is_credit_memo"),
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.is_credit_memo}/>,
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.exported"),
|
||||
dataIndex: "exported",
|
||||
key: "exported",
|
||||
sorter: (a, b) => a.exported - b.exported,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.exported}/>,
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => recordActions(record, true),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
|
||||
const filteredBills = bills
|
||||
? searchText === ""
|
||||
? bills
|
||||
: bills.filter(
|
||||
(b) =>
|
||||
(b.invoice_number || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(b.vendor.name || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(b.total || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("bills.labels.bills")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined/>
|
||||
</Button>
|
||||
{job && job.converted ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: {refetch: billsQuery.refetch},
|
||||
context: {
|
||||
job,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.postbills")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setReconciliationContext({
|
||||
actions: {refetch: billsQuery.refetch},
|
||||
context: {
|
||||
job,
|
||||
bills: (billsQuery.data && billsQuery.data.bills) || [],
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.reconcile")}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
x: true, // y: "50rem"
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
{job && job.converted ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
context: {
|
||||
job
|
||||
}
|
||||
});
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredBills}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
>
|
||||
{t("jobs.actions.postbills")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setReconciliationContext({
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
context: {
|
||||
job,
|
||||
bills: (billsQuery.data && billsQuery.data.bills) || []
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.reconcile")}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
x: true // y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredBills}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillsListTableComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);
|
||||
|
||||
@@ -1,121 +1,112 @@
|
||||
import React, {useState} from "react";
|
||||
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
|
||||
import {useQuery} from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {Input, Table} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Input, Table } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
export default function BillsVendorsList() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
|
||||
const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: "",
|
||||
});
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("vendors.fields.name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
sorter: (a, b) => alphaSort(a.name, b.name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
|
||||
const columns = [
|
||||
{
|
||||
title: t("vendors.fields.name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
sorter: (a, b) => alphaSort(a.name, b.name),
|
||||
sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("vendors.fields.cost_center"),
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("vendors.fields.city"),
|
||||
dataIndex: "city",
|
||||
key: "city"
|
||||
}
|
||||
];
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
delete search.billid;
|
||||
if (record.id) {
|
||||
search.vendorid = record.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.vendorid;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({ ...state, search: e.target.value });
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const dataSource = state.search
|
||||
? data.vendors.filter(
|
||||
(v) =>
|
||||
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: (data && data.vendors) || [];
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
{
|
||||
title: t("vendors.fields.cost_center"),
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("vendors.fields.city"),
|
||||
dataIndex: "city",
|
||||
key: "city",
|
||||
},
|
||||
];
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
delete search.billid;
|
||||
if (record.id) {
|
||||
search.vendorid = record.id;
|
||||
history.push({search: queryString.stringify(search)});
|
||||
}
|
||||
} else {
|
||||
delete search.vendorid;
|
||||
history.push({search: queryString.stringify(search)});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setState({...state, search: e.target.value});
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
|
||||
const dataSource = state.search
|
||||
? data.vendors.filter(
|
||||
(v) =>
|
||||
(v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(v.cost_center || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(v.city || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
)
|
||||
: (data && data.vendors) || [];
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
title={() => {
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
placeholder={t("general.labels.search")}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
pagination={{position: "top"}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [search.vendorid],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
}, // click row
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
selectedRowKeys: [search.vendorid],
|
||||
type: "radio"
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
} // click row
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
import {HomeFilled} from "@ant-design/icons";
|
||||
import {Breadcrumb, Col, Row} from "antd";
|
||||
import { HomeFilled } from "@ant-design/icons";
|
||||
import { Breadcrumb, Col, Row } from "antd";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
breadcrumbs: selectBreadcrumbs,
|
||||
bodyshop: selectBodyshop,
|
||||
breadcrumbs: selectBreadcrumbs,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function BreadCrumbs({breadcrumbs, bodyshop}) {
|
||||
|
||||
const {treatments: {OpenSearch}} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["OpenSearch"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid,
|
||||
});
|
||||
// TODO - Client Update - Technically key is not doing anything here
|
||||
return (
|
||||
<Row className="breadcrumb-container">
|
||||
<Col xs={24} sm={24} md={16}>
|
||||
<Breadcrumb
|
||||
separator=">"
|
||||
items={[
|
||||
{
|
||||
key: "home",
|
||||
title: (
|
||||
<Link to={`/manage/`}>
|
||||
<HomeFilled/>{" "}
|
||||
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
||||
""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
...breadcrumbs.map((item) =>
|
||||
item.link
|
||||
? {
|
||||
key: item.label,
|
||||
title: <Link to={item.link}>{item.label}</Link>,
|
||||
}
|
||||
: {
|
||||
key: item.label,
|
||||
title: item.label,
|
||||
}
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
||||
const {
|
||||
treatments: { OpenSearch }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["OpenSearch"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
// TODO - Client Update - Technically key is not doing anything here
|
||||
return (
|
||||
<Row className="breadcrumb-container">
|
||||
<Col xs={24} sm={24} md={16}>
|
||||
<Breadcrumb
|
||||
separator=">"
|
||||
items={[
|
||||
{
|
||||
key: "home",
|
||||
title: (
|
||||
<Link to={`/manage/`}>
|
||||
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
...breadcrumbs.map((item) =>
|
||||
item.link
|
||||
? {
|
||||
key: item.label,
|
||||
title: <Link to={item.link}>{item.label}</Link>
|
||||
}
|
||||
: {
|
||||
key: item.label,
|
||||
title: item.label
|
||||
}
|
||||
)
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||
|
||||
@@ -1,99 +1,87 @@
|
||||
import {Button, Form, Modal} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import { Button, Form, Modal } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
caBcEtfTableModal: selectCaBcEtfTableConvert,
|
||||
caBcEtfTableModal: selectCaBcEtfTableConvert
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () =>
|
||||
dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
|
||||
});
|
||||
|
||||
export function ContractsFindModalContainer({
|
||||
caBcEtfTableModal,
|
||||
toggleModalVisible,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {open} = caBcEtfTableModal;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
||||
const { open } = caBcEtfTableModal;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("ca_bc_etf_table_parse");
|
||||
setLoading(true);
|
||||
const claimNumbers = [];
|
||||
values.table.split("\n").forEach((row, idx, arr) => {
|
||||
const {1: claim, 2: shortclaim, 4: amount} = row.split("\t");
|
||||
if (!claim || !shortclaim) return;
|
||||
const trimmedShortClaim = shortclaim.trim();
|
||||
// const trimmedClaim = claim.trim();
|
||||
if (amount.slice(-1) === "-") {
|
||||
}
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("ca_bc_etf_table_parse");
|
||||
setLoading(true);
|
||||
const claimNumbers = [];
|
||||
values.table.split("\n").forEach((row, idx, arr) => {
|
||||
const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
|
||||
if (!claim || !shortclaim) return;
|
||||
const trimmedShortClaim = shortclaim.trim();
|
||||
// const trimmedClaim = claim.trim();
|
||||
if (amount.slice(-1) === "-") {
|
||||
}
|
||||
|
||||
claimNumbers.push({
|
||||
claim: trimmedShortClaim,
|
||||
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount,
|
||||
});
|
||||
});
|
||||
claimNumbers.push({
|
||||
claim: trimmedShortClaim,
|
||||
amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
|
||||
});
|
||||
});
|
||||
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: EtfTemplate.key,
|
||||
variables: {
|
||||
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
|
||||
claimdata: claimNumbers,
|
||||
},
|
||||
},
|
||||
{},
|
||||
values.sendby === "email" ? "e" : "p"
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: EtfTemplate.key,
|
||||
variables: {
|
||||
claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
|
||||
claimdata: claimNumbers
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width="70%"
|
||||
title={t("payments.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="no"
|
||||
onFinish={handleFinish}
|
||||
>
|
||||
<CaBcEtfTableModalComponent form={form}/>
|
||||
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
||||
{t("general.labels.search")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
},
|
||||
{},
|
||||
values.sendby === "email" ? "e" : "p"
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width="70%"
|
||||
title={t("payments.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||
<CaBcEtfTableModalComponent form={form} />
|
||||
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
||||
{t("general.labels.search")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ContractsFindModalContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import {Form, Input, Radio} from "antd";
|
||||
import { Form, Input, Radio } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
|
||||
|
||||
export function PartsReceiveModalComponent({bodyshop, form}) {
|
||||
const {t} = useTranslation();
|
||||
export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="table"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={8}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendby"
|
||||
initialValue="print"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
name="table"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={8} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
|
||||
<Radio.Group>
|
||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
import React, {useState} from "react";
|
||||
import {Button, Form, InputNumber, Popover} from "antd";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {CalculatorFilled} from "@ant-design/icons";
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, InputNumber, Popover } from "antd";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CalculatorFilled } from "@ant-design/icons";
|
||||
|
||||
export default function CABCpvrtCalculator({disabled, form}) {
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||
form.setFieldsValue({
|
||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
||||
});
|
||||
form.setFields([{name: "ca_bc_pvrt", touched: true}]);
|
||||
setVisibility(false);
|
||||
};
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||
form.setFieldsValue({
|
||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2)
|
||||
});
|
||||
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish} initialValues={{rate: 1.5}}>
|
||||
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
||||
<InputNumber precision={2} min={0}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||
<InputNumber precision={0} min={0}/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.calculate")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish} initialValues={{ rate: 1.5 }}>
|
||||
<Form.Item name="rate" label={t("jobs.labels.ca_bc_pvrt.rate")}>
|
||||
<InputNumber precision={2} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.calculate")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={popContent}
|
||||
open={visibility}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
<CalculatorFilled/>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
<CalculatorFilled />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,357 +1,328 @@
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import {useLazyQuery, useMutation} from "@apollo/client";
|
||||
import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd";
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries";
|
||||
import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectCardPayment} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type})),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({
|
||||
bodyshop,
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail,
|
||||
}) => {
|
||||
const {context} = cardPaymentModal;
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const {t} = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [, {data, refetch, queryLoading}] = useLazyQuery(
|
||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
||||
{
|
||||
variables: {jobids: [context.jobid]},
|
||||
skip: true,
|
||||
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||
variables: { jobids: [context.jobid] },
|
||||
skip: true
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
window.intellipay.runOnClose(() => {
|
||||
//window.intellipay.initialize();
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: payment.jobid,
|
||||
declinereason: response.declinereason,
|
||||
ext_paymentid: response.paymentid.toString(),
|
||||
successful: false,
|
||||
response
|
||||
}))
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
window.intellipay.runOnClose(() => {
|
||||
//window.intellipay.initialize();
|
||||
});
|
||||
payments.forEach((payment) =>
|
||||
insertAuditTrail({
|
||||
jobid: payment.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
type: "failedpayment"
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
});
|
||||
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: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const {payments} = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: payment.jobid,
|
||||
declinereason: response.declinereason,
|
||||
ext_paymentid: response.paymentid.toString(),
|
||||
successful: false,
|
||||
response,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
payments.forEach((payment) =>
|
||||
insertAuditTrail({
|
||||
jobid: payment.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
type: "failedpayment",})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: values.payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: payment.jobid,
|
||||
date: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse,
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"],
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("payments.errors.inserting", {error: error.message}),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(response.data);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.autoOpen();
|
||||
} else {
|
||||
var rg = document.createRange();
|
||||
let node = rg.createContextualFragment(response.data);
|
||||
document.documentElement.appendChild(node);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.isAutoOpen = true;
|
||||
window.intellipay.initialize();
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip"),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}))
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
payments: context.jobid ? [{jobid: context.jobid}] : [],
|
||||
}}
|
||||
>
|
||||
<Form.List name={["payments"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
key={`${index}jobid`}
|
||||
label={t("jobs.fields.ro_number")}
|
||||
name={[field.name, "jobid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<JobSearchSelectComponent
|
||||
notExported={false}
|
||||
clm_no
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
key={`${index}amount`}
|
||||
label={t("payments.fields.amount")}
|
||||
name={[field.name, "amount"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyFormItemComponent/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{margin: "1rem"}}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{width: "100%"}}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.jobid).join() !==
|
||||
curValues.payments?.map((p) => p?.jobid).join()
|
||||
}
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(response.data);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.autoOpen();
|
||||
} else {
|
||||
var rg = document.createRange();
|
||||
let node = rg.createContextualFragment(response.data);
|
||||
document.documentElement.appendChild(node);
|
||||
SetIntellipayCallbackFunctions();
|
||||
window.intellipay.isAutoOpen = true;
|
||||
window.intellipay.initialize();
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip")
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
payments: context.jobid ? [{ jobid: context.jobid }] : []
|
||||
}}
|
||||
>
|
||||
<Form.List name={["payments"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
key={`${index}jobid`}
|
||||
label={t("jobs.fields.ro_number")}
|
||||
name={[field.name, "jobid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<JobSearchSelectComponent notExported={false} clm_no />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
key={`${index}amount`}
|
||||
label={t("payments.fields.amount")}
|
||||
name={[field.name, "amount"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CurrencyFormItemComponent />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{ margin: "1rem" }}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const {payments} = form.getFieldsValue();
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
console.log("**Calling refetch.");
|
||||
refetch({jobids: payments.map((p) => p.jobid)});
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0
|
||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||
: null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="account"
|
||||
type="hidden"
|
||||
value={
|
||||
payments && data && data.jobs.length > 0
|
||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="email"
|
||||
type="hidden"
|
||||
value={
|
||||
payments && data && data.jobs.length > 0
|
||||
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.amount).join() !==
|
||||
curValues.payments?.map((p) => p?.amount).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const {payments} = form.getFieldsValue();
|
||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0);
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
|
||||
return (
|
||||
<Space style={{float: "right"}}>
|
||||
<Statistic
|
||||
title="Amount To Charge"
|
||||
value={totalAmountToCharge}
|
||||
precision={2}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="amount"
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
loading={queryLoading || loading}
|
||||
disabled={!(totalAmountToCharge > 0)}
|
||||
onClick={handleIntelliPayCharge}
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
console.log("**Calling refetch.");
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="account"
|
||||
type="hidden"
|
||||
value={
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="email"
|
||||
type="hidden"
|
||||
value={
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const { payments } = form.getFieldsValue();
|
||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0);
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden"/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Space style={{ float: "right" }}>
|
||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="amount"
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
loading={queryLoading || loading}
|
||||
disabled={!(totalAmountToCharge > 0)}
|
||||
onClick={handleIntelliPayCharge}
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CardPaymentModalComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
|
||||
|
||||
@@ -1,57 +1,50 @@
|
||||
import {Button, Modal} from "antd";
|
||||
import { Button, Modal } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectCardPayment} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CardPaymentModalComponent from "./card-payment-modal.component.";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||
});
|
||||
|
||||
function CardPaymentModalContainer({
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
}) {
|
||||
const {open} = cardPaymentModal;
|
||||
const {t} = useTranslation();
|
||||
function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
|
||||
const { open } = cardPaymentModal;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCancel = () => {
|
||||
toggleModalVisible();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
toggleModalVisible();
|
||||
};
|
||||
|
||||
const handleOK = () => {
|
||||
toggleModalVisible();
|
||||
};
|
||||
const handleOK = () => {
|
||||
toggleModalVisible();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={handleOK}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<Button key="back" onClick={handleCancel}>
|
||||
{t("job_payments.buttons.goback")}
|
||||
</Button>,
|
||||
]}
|
||||
width="80%"
|
||||
destroyOnClose
|
||||
>
|
||||
<CardPaymentModalComponent/>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={handleOK}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<Button key="back" onClick={handleCancel}>
|
||||
{t("job_payments.buttons.goback")}
|
||||
</Button>
|
||||
]}
|
||||
width="80%"
|
||||
destroyOnClose
|
||||
>
|
||||
<CardPaymentModalComponent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CardPaymentModalContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer);
|
||||
|
||||
@@ -1,100 +1,97 @@
|
||||
import {useApolloClient} from "@apollo/client";
|
||||
import {getToken, onMessage} from "@firebase/messaging";
|
||||
import {Button, notification, Space} from "antd";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { getToken, onMessage } from "@firebase/messaging";
|
||||
import { Button, notification, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {messaging, requestForToken} from "../../firebase/firebase.utils";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||
import FcmHandler from "../../utils/fcm-handler";
|
||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
|
||||
export function ChatAffixContainer({bodyshop, chatVisible}) {
|
||||
const {t} = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
async function SubscribeToTopic() {
|
||||
try {
|
||||
const r = await axios.post("/notifications/subscribe", {
|
||||
fcm_tokens: await getToken(messaging, {
|
||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
|
||||
}),
|
||||
type: "messaging",
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
});
|
||||
console.log("FCM Topic Subscription", r.data);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Error attempting to subscribe to messaging topic: ",
|
||||
error
|
||||
);
|
||||
notification.open({
|
||||
key: 'fcm',
|
||||
type: "warning",
|
||||
message: t("general.errors.fcm"),
|
||||
btn: (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await requestForToken();
|
||||
SubscribeToTopic();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.tryagain")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const win = window.open(
|
||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
||||
"_blank"
|
||||
);
|
||||
win.focus();
|
||||
}}
|
||||
>
|
||||
{t("general.labels.help")}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||
|
||||
SubscribeToTopic();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bodyshop]);
|
||||
async function SubscribeToTopic() {
|
||||
try {
|
||||
const r = await axios.post("/notifications/subscribe", {
|
||||
fcm_tokens: await getToken(messaging, {
|
||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
||||
}),
|
||||
type: "messaging",
|
||||
imexshopid: bodyshop.imexshopid
|
||||
});
|
||||
console.log("FCM Topic Subscription", r.data);
|
||||
} catch (error) {
|
||||
console.log("Error attempting to subscribe to messaging topic: ", error);
|
||||
notification.open({
|
||||
key: "fcm",
|
||||
type: "warning",
|
||||
message: t("general.errors.fcm"),
|
||||
btn: (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await requestForToken();
|
||||
SubscribeToTopic();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.tryagain")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const win = window.open(
|
||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
||||
"_blank"
|
||||
);
|
||||
win.focus();
|
||||
}}
|
||||
>
|
||||
{t("general.labels.help")}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage(payload) {
|
||||
FcmHandler({
|
||||
client,
|
||||
payload: (payload && payload.data && payload.data.data) || payload.data,
|
||||
});
|
||||
}
|
||||
SubscribeToTopic();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bodyshop]);
|
||||
|
||||
let stopMessageListener, channel;
|
||||
try {
|
||||
stopMessageListener = onMessage(messaging, handleMessage);
|
||||
channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.addEventListener("message", handleMessage);
|
||||
} catch (error) {
|
||||
console.log("Unable to set event listeners.");
|
||||
}
|
||||
return () => {
|
||||
stopMessageListener && stopMessageListener();
|
||||
channel && channel.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [client]);
|
||||
useEffect(() => {
|
||||
function handleMessage(payload) {
|
||||
FcmHandler({
|
||||
client,
|
||||
payload: (payload && payload.data && payload.data.data) || payload.data
|
||||
});
|
||||
}
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
let stopMessageListener, channel;
|
||||
try {
|
||||
stopMessageListener = onMessage(messaging, handleMessage);
|
||||
channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.addEventListener("message", handleMessage);
|
||||
} catch (error) {
|
||||
console.log("Unable to set event listeners.");
|
||||
}
|
||||
return () => {
|
||||
stopMessageListener && stopMessageListener();
|
||||
channel && channel.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
|
||||
</div>
|
||||
);
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatAffixContainer;
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {TOGGLE_CONVERSATION_ARCHIVE} from "../../graphql/conversations.queries";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||
|
||||
export default function ChatArchiveButton({conversation}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {t} = useTranslation();
|
||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||
const handleToggleArchive = async () => {
|
||||
setLoading(true);
|
||||
export default function ChatArchiveButton({ conversation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||
const handleToggleArchive = async () => {
|
||||
setLoading(true);
|
||||
|
||||
await updateConversation({
|
||||
variables: {id: conversation.id, archived: !conversation.archived},
|
||||
refetchQueries: ["CONVERSATION_LIST_QUERY"],
|
||||
});
|
||||
await updateConversation({
|
||||
variables: { id: conversation.id, archived: !conversation.archived },
|
||||
refetchQueries: ["CONVERSATION_LIST_QUERY"]
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
||||
{conversation.archived
|
||||
? t("messaging.labels.unarchive")
|
||||
: t("messaging.labels.archive")}
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
||||
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,122 +1,101 @@
|
||||
import {Badge, Card, List, Space, Tag} from "antd";
|
||||
import { Badge, Card, List, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
|
||||
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
||||
import {TimeAgoFormatter} from "../../utils/DateFormatter";
|
||||
import { connect } from "react-redux";
|
||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import _ from "lodash";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
selectedConversation: selectSelectedConversation
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedConversation: (conversationId) =>
|
||||
dispatch(setSelectedConversation(conversationId)),
|
||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||
});
|
||||
|
||||
function ChatConversationListComponent({
|
||||
conversationList,
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
loadMoreConversations,
|
||||
}) {
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 60,
|
||||
});
|
||||
conversationList,
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
loadMoreConversations
|
||||
}) {
|
||||
const cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 60
|
||||
});
|
||||
|
||||
const rowRenderer = ({index, key, style, parent}) => {
|
||||
const item = conversationList[index];
|
||||
const cardContentRight =
|
||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||
const cardContentLeft = item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx}>{j.job.ro_number}</Tag>
|
||||
))
|
||||
: null;
|
||||
const rowRenderer = ({ index, key, style, parent }) => {
|
||||
const item = conversationList[index];
|
||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||
const cardContentLeft =
|
||||
item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => <Tag key={idx}>{j.job.ro_number}</Tag>)
|
||||
: null;
|
||||
|
||||
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
|
||||
OwnerNameDisplayFunction(j.job)
|
||||
))}</>
|
||||
const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}</>;
|
||||
|
||||
const cardTitle = <>
|
||||
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<Space direction="vertical">
|
||||
{names}
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
|
||||
const cardTitle = (
|
||||
<>
|
||||
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<Space direction="vertical">{names}</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
||||
|
||||
const getCardStyle = () =>
|
||||
item.id === selectedConversation
|
||||
? {backgroundColor: 'rgba(128, 128, 128, 0.2)'}
|
||||
: {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'};
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
columnIndex={0}
|
||||
rowIndex={index}
|
||||
>
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
style={style}
|
||||
className={`chat-list-item
|
||||
${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
|
||||
{cardContentLeft}
|
||||
</div>
|
||||
<div
|
||||
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
const getCardStyle = () =>
|
||||
item.id === selectedConversation
|
||||
? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
|
||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<AutoSizer>
|
||||
{({height, width}) => (
|
||||
<VirtualizedList
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={conversationList.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
|
||||
if (scrollTop + clientHeight === scrollHeight) {
|
||||
loadMoreConversations();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
style={style}
|
||||
className={`chat-list-item
|
||||
${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<VirtualizedList
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={conversationList.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
||||
if (scrollTop + clientHeight === scrollHeight) {
|
||||
loadMoreConversations();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatConversationListComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationListComponent);
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Tag} from "antd";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Tag } from "antd";
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {REMOVE_CONVERSATION_TAG} from "../../graphql/job-conversations.queries";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function ChatConversationTitleTags({jobConversations}) {
|
||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||
|
||||
const handleRemoveTag = (jobId) => {
|
||||
const convId = jobConversations[0].conversationid;
|
||||
if (!!convId) {
|
||||
removeJobConversation({
|
||||
variables: {
|
||||
conversationId: convId,
|
||||
jobId: jobId,
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({id: convId, __typename: "conversations"}),
|
||||
fields: {
|
||||
job_conversations(ex) {
|
||||
return ex.filter((e) => e.jobid !== jobId);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
logImEXEvent("messaging_remove_job_tag", {
|
||||
conversationId: convId,
|
||||
jobId: jobId,
|
||||
});
|
||||
const handleRemoveTag = (jobId) => {
|
||||
const convId = jobConversations[0].conversationid;
|
||||
if (!!convId) {
|
||||
removeJobConversation({
|
||||
variables: {
|
||||
conversationId: convId,
|
||||
jobId: jobId
|
||||
},
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({ id: convId, __typename: "conversations" }),
|
||||
fields: {
|
||||
job_conversations(ex) {
|
||||
return ex.filter((e) => e.jobid !== jobId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
logImEXEvent("messaging_remove_job_tag", {
|
||||
conversationId: convId,
|
||||
jobId: jobId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{jobConversations.map((item) => (
|
||||
<Tag
|
||||
key={item.job.id}
|
||||
closable
|
||||
color="blue"
|
||||
style={{cursor: "pointer"}}
|
||||
onClose={() => handleRemoveTag(item.job.id)}
|
||||
>
|
||||
<Link to={`/manage/jobs/${item.job.id}`}>
|
||||
{`${item.job.ro_number || "?"} | `}
|
||||
<OwnerNameDisplay ownerObject={item.job}/>
|
||||
</Link>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{jobConversations.map((item) => (
|
||||
<Tag
|
||||
key={item.job.id}
|
||||
closable
|
||||
color="blue"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClose={() => handleRemoveTag(item.job.id)}
|
||||
>
|
||||
<Link to={`/manage/jobs/${item.job.id}`}>
|
||||
{`${item.job.ro_number || "?"} | `}
|
||||
<OwnerNameDisplay ownerObject={item.job} />
|
||||
</Link>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Space} from "antd";
|
||||
import { Space } from "antd";
|
||||
import React from "react";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||
@@ -7,21 +7,15 @@ import ChatLabelComponent from "../chat-label/chat-label.component";
|
||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
|
||||
export default function ChatConversationTitle({conversation}) {
|
||||
return (
|
||||
<Space wrap>
|
||||
<PhoneNumberFormatter>
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
<ChatLabelComponent conversation={conversation}/>
|
||||
<ChatPrintButton conversation={conversation}/>
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []}/>
|
||||
<ChatArchiveButton conversation={conversation}/>
|
||||
</Space>
|
||||
);
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<Space wrap>
|
||||
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
||||
<ChatLabelComponent conversation={conversation} />
|
||||
<ChatPrintButton conversation={conversation} />
|
||||
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
<ChatArchiveButton conversation={conversation} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,26 +6,21 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||
import "./chat-conversation.styles.scss";
|
||||
|
||||
export default function ChatConversationComponent({
|
||||
subState,
|
||||
conversation,
|
||||
messages,
|
||||
handleMarkConversationAsRead,
|
||||
}) {
|
||||
const [loading, error] = subState;
|
||||
export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
|
||||
const [loading, error] = subState;
|
||||
|
||||
if (loading) return <LoadingSkeleton/>;
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-conversation"
|
||||
onMouseDown={handleMarkConversationAsRead}
|
||||
onKeyDown={handleMarkConversationAsRead}
|
||||
>
|
||||
<ChatConversationTitle conversation={conversation}/>
|
||||
<ChatMessageListComponent messages={messages}/>
|
||||
<ChatSendMessage conversation={conversation}/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="chat-conversation"
|
||||
onMouseDown={handleMarkConversationAsRead}
|
||||
onKeyDown={handleMarkConversationAsRead}
|
||||
>
|
||||
<ChatConversationTitle conversation={conversation} />
|
||||
<ChatMessageListComponent messages={messages} />
|
||||
<ChatSendMessage conversation={conversation} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,87 +1,81 @@
|
||||
import {useMutation, useQuery, useSubscription} from "@apollo/client";
|
||||
import React, {useState} from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries";
|
||||
import {MARK_MESSAGES_AS_READ_BY_CONVERSATION} from "../../graphql/messages.queries";
|
||||
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationComponent from "./chat-conversation.component";
|
||||
import axios from "axios";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop,
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
||||
|
||||
export function ChatConversationContainer({bodyshop, selectedConversation}) {
|
||||
const {
|
||||
loading: convoLoading,
|
||||
error: convoError,
|
||||
data: convoData,
|
||||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||||
variables: {conversationId: selectedConversation},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||
const {
|
||||
loading: convoLoading,
|
||||
error: convoError,
|
||||
data: convoData
|
||||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||||
variables: { conversationId: selectedConversation },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const {loading, error, data} = useSubscription(
|
||||
CONVERSATION_SUBSCRIPTION_BY_PK,
|
||||
{
|
||||
variables: {conversationId: selectedConversation},
|
||||
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||
variables: { conversationId: selectedConversation }
|
||||
});
|
||||
|
||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||
|
||||
const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
||||
variables: { conversationId: selectedConversation },
|
||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({
|
||||
__typename: "conversations",
|
||||
id: selectedConversation
|
||||
}),
|
||||
fields: {
|
||||
messages_aggregate(cached) {
|
||||
return { aggregate: { count: 0 } };
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||
const unreadCount =
|
||||
data &&
|
||||
data.messages &&
|
||||
data.messages.reduce((acc, val) => {
|
||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||
}, 0);
|
||||
|
||||
const [markConversationRead] = useMutation(
|
||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||
{
|
||||
variables: {conversationId: selectedConversation},
|
||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({
|
||||
__typename: "conversations",
|
||||
id: selectedConversation,
|
||||
}),
|
||||
fields: {
|
||||
messages_aggregate(cached) {
|
||||
return {aggregate: {count: 0}};
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
const handleMarkConversationAsRead = async () => {
|
||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||
setMarkingAsReadInProgress(true);
|
||||
await markConversationRead({});
|
||||
await axios.post("/sms/markConversationRead", {
|
||||
conversationid: selectedConversation,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
});
|
||||
setMarkingAsReadInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const unreadCount =
|
||||
data &&
|
||||
data.messages &&
|
||||
data.messages.reduce((acc, val) => {
|
||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
||||
}, 0);
|
||||
|
||||
const handleMarkConversationAsRead = async () => {
|
||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
||||
setMarkingAsReadInProgress(true);
|
||||
await markConversationRead({});
|
||||
await axios.post("/sms/markConversationRead", {
|
||||
conversationid: selectedConversation,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
});
|
||||
setMarkingAsReadInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatConversationComponent
|
||||
subState={[loading || convoLoading, error || convoError]}
|
||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||
messages={data ? data.messages : []}
|
||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ChatConversationComponent
|
||||
subState={[loading || convoLoading, error || convoError]}
|
||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||
messages={data ? data.messages : []}
|
||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,59 @@
|
||||
import {PlusOutlined} from "@ant-design/icons";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Input, notification, Spin, Tag, Tooltip} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {UPDATE_CONVERSATION_LABEL} from "../../graphql/conversations.queries";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||
|
||||
export default function ChatLabel({conversation}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [value, setValue] = useState(conversation.label);
|
||||
export default function ChatLabel({ conversation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [value, setValue] = useState(conversation.label);
|
||||
|
||||
const {t} = useTranslation();
|
||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||
const { t } = useTranslation();
|
||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await updateLabel({
|
||||
variables: {id: conversation.id, label: value},
|
||||
});
|
||||
if (response.errors) {
|
||||
notification["error"]({
|
||||
message: t("messages.errors.updatinglabel", {
|
||||
error: JSON.stringify(response.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
setEditing(false);
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({
|
||||
message: t("messages.errors.updatinglabel", {
|
||||
error: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (editing) {
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
allowClear
|
||||
/>
|
||||
{loading && <Spin size="small"/>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return conversation.label && conversation.label.trim() !== "" ? (
|
||||
<Tag style={{cursor: "pointer"}} onClick={() => setEditing(true)}>
|
||||
{conversation.label}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tooltip title={t("messaging.labels.addlabel")}>
|
||||
<PlusOutlined
|
||||
style={{cursor: "pointer"}}
|
||||
onClick={() => setEditing(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await updateLabel({
|
||||
variables: { id: conversation.id, label: value }
|
||||
});
|
||||
if (response.errors) {
|
||||
notification["error"]({
|
||||
message: t("messages.errors.updatinglabel", {
|
||||
error: JSON.stringify(response.errors)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
setEditing(false);
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({
|
||||
message: t("messages.errors.updatinglabel", {
|
||||
error: JSON.stringify(error)
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (editing) {
|
||||
return (
|
||||
<div>
|
||||
<Input autoFocus value={value} onChange={(e) => setValue(e.target.value)} onBlur={handleSave} allowClear />
|
||||
{loading && <Spin size="small" />}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return conversation.label && conversation.label.trim() !== "" ? (
|
||||
<Tag style={{ cursor: "pointer" }} onClick={() => setEditing(true)}>
|
||||
{conversation.label}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tooltip title={t("messaging.labels.addlabel")}>
|
||||
<PlusOutlined style={{ cursor: "pointer" }} onClick={() => setEditing(true)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,82 @@
|
||||
import {PictureFilled} from "@ant-design/icons";
|
||||
import {useQuery} from "@apollo/client";
|
||||
import {Badge, Popover} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { PictureFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Badge, Popover } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||
import JobDocumentsLocalGalleryExternal
|
||||
from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||
|
||||
export function ChatMediaSelector({
|
||||
bodyshop,
|
||||
selectedMedia,
|
||||
setSelectedMedia,
|
||||
conversation,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {loading, error, data} = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
jobId:
|
||||
conversation.job_conversations[0] &&
|
||||
conversation.job_conversations[0].jobid,
|
||||
},
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
||||
},
|
||||
|
||||
skip:
|
||||
!open ||
|
||||
!conversation.job_conversations ||
|
||||
conversation.job_conversations.length === 0,
|
||||
});
|
||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||
});
|
||||
|
||||
const handleVisibleChange = (change) => {
|
||||
setOpen(change);
|
||||
};
|
||||
const handleVisibleChange = (change) => {
|
||||
setOpen(change);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMedia([]);
|
||||
}, [setSelectedMedia, conversation]);
|
||||
useEffect(() => {
|
||||
setSelectedMedia([]);
|
||||
}, [setSelectedMedia, conversation]);
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
{loading && <LoadingSpinner/>}
|
||||
{error && <AlertComponent message={error.message} type="error"/>}
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{color: "red"}}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={
|
||||
conversation.job_conversations[0] &&
|
||||
conversation.job_conversations[0].jobid
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const content = (
|
||||
<div>
|
||||
{loading && <LoadingSpinner />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{!bodyshop.uselocalmediaserver && data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
conversation.job_conversations.length === 0 ? (
|
||||
<div>{t("messaging.errors.noattachedjobs")}</div>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
title={t("messaging.labels.selectmedia")}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleVisibleChange}
|
||||
>
|
||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||
<PictureFilled style={{margin: "0 .5rem"}}/>
|
||||
</Badge>
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content
|
||||
}
|
||||
title={t("messaging.labels.selectmedia")}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleVisibleChange}
|
||||
>
|
||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||
</Badge>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,113 +1,106 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import {Tooltip} from "antd";
|
||||
import { Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useEffect, useRef} from "react";
|
||||
import {MdDone, MdDoneAll} from "react-icons/md";
|
||||
import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import "./chat-message-list.styles.scss";
|
||||
|
||||
export default function ChatMessageListComponent({messages}) {
|
||||
const virtualizedListRef = useRef(null);
|
||||
export default function ChatMessageListComponent({ messages }) {
|
||||
const virtualizedListRef = useRef(null);
|
||||
|
||||
const _cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
// minHeight: 50,
|
||||
defaultHeight: 100,
|
||||
});
|
||||
const _cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
// minHeight: 50,
|
||||
defaultHeight: 100
|
||||
});
|
||||
|
||||
const scrollToBottom = (renderedrows) => {
|
||||
//console.log("Scrolling to", messages.length);
|
||||
// !!virtualizedListRef.current &&
|
||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
||||
//Scrolling does not work on this version of React.
|
||||
};
|
||||
const scrollToBottom = (renderedrows) => {
|
||||
//console.log("Scrolling to", messages.length);
|
||||
// !!virtualizedListRef.current &&
|
||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
||||
//Scrolling does not work on this version of React.
|
||||
};
|
||||
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
|
||||
const _rowRenderer = ({index, key, parent, style}) => {
|
||||
return (
|
||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
||||
{({measure, registerChild}) => (
|
||||
<div
|
||||
ref={registerChild}
|
||||
onLoad={measure}
|
||||
style={style}
|
||||
className={`${
|
||||
messages[index].isoutbound ? "mine messages" : "yours messages"
|
||||
}`}
|
||||
>
|
||||
<div className="message msgmargin">
|
||||
{MessageRender(messages[index])}
|
||||
{StatusRender(messages[index].status)}
|
||||
</div>
|
||||
{messages[index].isoutbound && (
|
||||
<div style={{fontSize: 10}}>
|
||||
{i18n.t("messaging.labels.sentby", {
|
||||
by: messages[index].userid,
|
||||
time: dayjs(messages[index].created_at).format(
|
||||
"MM/DD/YYYY @ hh:mm a"
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
useEffect(scrollToBottom, [messages]);
|
||||
|
||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
||||
return (
|
||||
<div className="chat">
|
||||
<AutoSizer>
|
||||
{({height, width}) => (
|
||||
<List
|
||||
ref={virtualizedListRef}
|
||||
width={width}
|
||||
height={height}
|
||||
rowHeight={_cache.rowHeight}
|
||||
rowRenderer={_rowRenderer}
|
||||
rowCount={messages.length}
|
||||
overscanRowCount={10}
|
||||
estimatedRowSize={150}
|
||||
scrollToIndex={messages.length}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
||||
{({ measure, registerChild }) => (
|
||||
<div
|
||||
ref={registerChild}
|
||||
onLoad={measure}
|
||||
style={style}
|
||||
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
||||
>
|
||||
<div className="message msgmargin">
|
||||
{MessageRender(messages[index])}
|
||||
{StatusRender(messages[index].status)}
|
||||
</div>
|
||||
{messages[index].isoutbound && (
|
||||
<div style={{ fontSize: 10 }}>
|
||||
{i18n.t("messaging.labels.sentby", {
|
||||
by: messages[index].userid,
|
||||
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={virtualizedListRef}
|
||||
width={width}
|
||||
height={height}
|
||||
rowHeight={_cache.rowHeight}
|
||||
rowRenderer={_rowRenderer}
|
||||
rowCount={messages.length}
|
||||
overscanRowCount={10}
|
||||
estimatedRowSize={150}
|
||||
scrollToIndex={messages.length}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MessageRender = (message) => {
|
||||
return (
|
||||
<Tooltip title={DateTimeFormatter({children: message.created_at})}>
|
||||
<div>
|
||||
{message.image_path &&
|
||||
message.image_path.map((i, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{display: "flex", justifyContent: "center"}}
|
||||
>
|
||||
<a href={i} target="__blank">
|
||||
<img alt="Received" className="message-img" src={i}/>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
return (
|
||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||
<div>
|
||||
{message.image_path &&
|
||||
message.image_path.map((i, idx) => (
|
||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||
<a href={i} target="__blank">
|
||||
<img alt="Received" className="message-img" src={i} />
|
||||
</a>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusRender = (status) => {
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <Icon component={MdDone} className="message-icon"/>;
|
||||
case "delivered":
|
||||
return <Icon component={MdDoneAll} className="message-icon"/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
switch (status) {
|
||||
case "sent":
|
||||
return <Icon component={MdDone} className="message-icon" />;
|
||||
case "delivered":
|
||||
return <Icon component={MdDoneAll} className="message-icon" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,55 +1,49 @@
|
||||
import {PlusCircleFilled} from "@ant-design/icons";
|
||||
import {Button, Form, Popover} from "antd";
|
||||
import { PlusCircleFilled } from "@ant-design/icons";
|
||||
import { Button, Form, Popover } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {openChatByPhone} from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
||||
});
|
||||
|
||||
export function ChatNewConversation({openChatByPhone}) {
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const handleFinish = (values) => {
|
||||
openChatByPhone({phone_num: values.phoneNumber});
|
||||
form.resetFields();
|
||||
};
|
||||
export function ChatNewConversation({ openChatByPhone }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const handleFinish = (values) => {
|
||||
openChatByPhone({ phone_num: values.phoneNumber });
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("messaging.labels.phonenumber")}
|
||||
name="phoneNumber"
|
||||
rules={[
|
||||
({getFieldValue}) =>
|
||||
PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
|
||||
]}
|
||||
>
|
||||
<PhoneFormItem/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("messaging.actions.new")}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("messaging.labels.phonenumber")}
|
||||
name="phoneNumber"
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "phoneNumber")]}
|
||||
>
|
||||
<PhoneFormItem />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("messaging.actions.new")}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover trigger="click" content={popContent}>
|
||||
<PlusCircleFilled/>
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Popover trigger="click" content={popContent}>
|
||||
<PlusCircleFilled />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatNewConversation);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatNewConversation);
|
||||
|
||||
@@ -1,54 +1,47 @@
|
||||
import {notification} from "antd";
|
||||
import { notification } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {openChatByPhone} from "../../redux/messaging/messaging.actions";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {searchingForConversation} from "../../redux/messaging/messaging.selectors";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
searchingForConversation: searchingForConversation,
|
||||
bodyshop: selectBodyshop,
|
||||
searchingForConversation: searchingForConversation
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
||||
});
|
||||
|
||||
export function ChatOpenButton({
|
||||
bodyshop,
|
||||
searchingForConversation,
|
||||
phone,
|
||||
jobid,
|
||||
openChatByPhone,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
if (!phone) return <></>;
|
||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||
const { t } = useTranslation();
|
||||
if (!phone) return <></>;
|
||||
|
||||
if (!bodyshop.messagingservicesid)
|
||||
return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||
|
||||
return (
|
||||
<a
|
||||
href="# "
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const p = parsePhoneNumber(phone, "CA");
|
||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({phone_num: p.formatInternational(), jobid: jobid});
|
||||
} else {
|
||||
notification["error"]({message: t("messaging.error.invalidphone")});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<a
|
||||
href="# "
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const p = parsePhoneNumber(phone, "CA");
|
||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
||||
} else {
|
||||
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons";
|
||||
import {useLazyQuery, useQuery} from "@apollo/client";
|
||||
import {Badge, Card, Col, Row, Space, Tag, Tooltip, Typography} from "antd";
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries";
|
||||
import {toggleChatVisible} from "../../redux/messaging/messaging.actions";
|
||||
import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors";
|
||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
|
||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||
@@ -15,119 +15,102 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import "./chat-popup.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
chatVisible: selectChatVisible,
|
||||
selectedConversation: selectSelectedConversation,
|
||||
chatVisible: selectChatVisible
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible()),
|
||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||
});
|
||||
|
||||
export function ChatPopupComponent({
|
||||
chatVisible,
|
||||
selectedConversation,
|
||||
toggleChatVisible,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [pollInterval, setpollInterval] = useState(0);
|
||||
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||
const { t } = useTranslation();
|
||||
const [pollInterval, setpollInterval] = useState(0);
|
||||
|
||||
const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
...(pollInterval > 0 ? {pollInterval} : {}),
|
||||
});
|
||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
const [getConversations, {loading, data, refetch, fetchMore}] =
|
||||
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !chatVisible,
|
||||
...(pollInterval > 0 ? {pollInterval} : {}),
|
||||
});
|
||||
const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !chatVisible,
|
||||
...(pollInterval > 0 ? { pollInterval } : {})
|
||||
});
|
||||
|
||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||
|
||||
useEffect(() => {
|
||||
if (fcmToken) {
|
||||
setpollInterval(0);
|
||||
} else {
|
||||
setpollInterval(60000);
|
||||
useEffect(() => {
|
||||
if (fcmToken) {
|
||||
setpollInterval(0);
|
||||
} else {
|
||||
setpollInterval(60000);
|
||||
}
|
||||
}, [fcmToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatVisible)
|
||||
getConversations({
|
||||
variables: {
|
||||
offset: 0
|
||||
}
|
||||
}, [fcmToken]);
|
||||
});
|
||||
}, [chatVisible, getConversations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatVisible)
|
||||
getConversations({
|
||||
variables: {
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
}, [chatVisible, getConversations]);
|
||||
const loadMoreConversations = useCallback(() => {
|
||||
if (data)
|
||||
fetchMore({
|
||||
variables: {
|
||||
offset: data.conversations.length
|
||||
}
|
||||
});
|
||||
}, [data, fetchMore]);
|
||||
|
||||
const loadMoreConversations = useCallback(() => {
|
||||
if (data)
|
||||
fetchMore({
|
||||
variables: {
|
||||
offset: data.conversations.length,
|
||||
},
|
||||
});
|
||||
}, [data, fetchMore]);
|
||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||
|
||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
<Card size="small">
|
||||
{chatVisible ? (
|
||||
<div className="chat-popup">
|
||||
<Space align="center">
|
||||
<Typography.Title level={4}>{t("messaging.labels.messaging")}</Typography.Title>
|
||||
<ChatNewConversation />
|
||||
<Tooltip title={t("messaging.labels.recentonly")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
||||
{pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
||||
</Space>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
<Card size="small">
|
||||
{chatVisible ? (
|
||||
<div className="chat-popup">
|
||||
<Space align="center">
|
||||
<Typography.Title level={4}>
|
||||
{t("messaging.labels.messaging")}
|
||||
</Typography.Title>
|
||||
<ChatNewConversation/>
|
||||
<Tooltip title={t("messaging.labels.recentonly")}>
|
||||
<InfoCircleOutlined/>
|
||||
</Tooltip>
|
||||
<SyncOutlined
|
||||
style={{cursor: "pointer"}}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
{pollInterval > 0 && (
|
||||
<Tag color="yellow">{t("messaging.labels.nopush")}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
<ShrinkOutlined
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{position: "absolute", right: ".5rem", top: ".5rem"}}
|
||||
/>
|
||||
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
{loading ? (
|
||||
<LoadingSpinner/>
|
||||
) : (
|
||||
<ChatConversationListComponent
|
||||
conversationList={data ? data.conversations : []}
|
||||
loadMoreConversations={loadMoreConversations}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
{selectedConversation ? <ChatConversationContainer/> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<div
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{cursor: "pointer"}}
|
||||
>
|
||||
<MessageOutlined className="chat-popup-info-icon"/>
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
<ChatConversationListComponent
|
||||
conversationList={data ? data.conversations : []}
|
||||
loadMoreConversations={loadMoreConversations}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
</Col>
|
||||
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<div onClick={() => toggleChatVisible()} style={{ cursor: "pointer" }}>
|
||||
<MessageOutlined className="chat-popup-info-icon" />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import {PlusCircleOutlined} from "@ant-design/icons";
|
||||
import {Dropdown} from "antd";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {setMessage} from "../../redux/messaging/messaging.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setMessage: (message) => dispatch(setMessage(message))
|
||||
});
|
||||
|
||||
export function ChatPresetsComponent({bodyshop, setMessage, className}) {
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: i.label,
|
||||
onClick: () => setMessage(i.text)
|
||||
}));
|
||||
|
||||
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: (i.label),
|
||||
onClick: () => setMessage(i.text),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown trigger={["click"]} menu={{items}}>
|
||||
<PlusCircleOutlined/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown trigger={["click"]} menu={{ items }}>
|
||||
<PlusCircleOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatPresetsComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPresetsComponent);
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import {MailOutlined, PrinterOutlined} from "@ant-design/icons";
|
||||
import {Space, Spin} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {setEmailOptions} from "../../redux/email/email.actions";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
||||
import { Space, Spin } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e))
|
||||
});
|
||||
|
||||
export function ChatPrintButton({conversation}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
export function ChatPrintButton({ conversation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const generateDocument = (type) => {
|
||||
setLoading(true);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("messaging").conversation_list.key,
|
||||
variables: {id: conversation.id},
|
||||
},
|
||||
{
|
||||
subject: TemplateList("messaging").conversation_list.subject,
|
||||
},
|
||||
type,
|
||||
conversation.id
|
||||
).catch(e => {
|
||||
console.warn('Something went wrong generating a document.');
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
const generateDocument = (type) => {
|
||||
setLoading(true);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("messaging").conversation_list.key,
|
||||
variables: { id: conversation.id }
|
||||
},
|
||||
{
|
||||
subject: TemplateList("messaging").conversation_list.subject
|
||||
},
|
||||
type,
|
||||
conversation.id
|
||||
).catch((e) => {
|
||||
console.warn("Something went wrong generating a document.");
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<PrinterOutlined onClick={() => generateDocument('p')}/>
|
||||
<MailOutlined onClick={() => generateDocument('e')}/>
|
||||
{loading && <Spin/>}
|
||||
</Space>
|
||||
);
|
||||
return (
|
||||
<Space wrap>
|
||||
<PrinterOutlined onClick={() => generateDocument("p")} />
|
||||
<MailOutlined onClick={() => generateDocument("e")} />
|
||||
{loading && <Spin />}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
||||
|
||||
@@ -1,111 +1,101 @@
|
||||
import {LoadingOutlined, SendOutlined} from "@ant-design/icons";
|
||||
import {Input, Spin} from "antd";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {sendMessage, setMessage,} from "../../redux/messaging/messaging.actions";
|
||||
import {selectIsSending, selectMessage,} from "../../redux/messaging/messaging.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { sendMessage, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
isSending: selectIsSending,
|
||||
message: selectMessage,
|
||||
bodyshop: selectBodyshop,
|
||||
isSending: selectIsSending,
|
||||
message: selectMessage
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
sendMessage: (message) => dispatch(sendMessage(message)),
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
sendMessage: (message) => dispatch(sendMessage(message)),
|
||||
setMessage: (message) => dispatch(setMessage(message))
|
||||
});
|
||||
|
||||
function ChatSendMessageComponent({
|
||||
conversation,
|
||||
bodyshop,
|
||||
sendMessage,
|
||||
isSending,
|
||||
message,
|
||||
setMessage,
|
||||
}) {
|
||||
const inputArea = useRef(null);
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||
const inputArea = useRef(null);
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||
logImEXEvent("messaging_send_message");
|
||||
const handleEnter = () => {
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||
logImEXEvent("messaging_send_message");
|
||||
|
||||
if (selectedImages.length < 11) {
|
||||
sendMessage({
|
||||
to: conversation.phone_num,
|
||||
body: message || "",
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
});
|
||||
setSelectedMedia(
|
||||
selectedMedia.map((i) => {
|
||||
return {...i, isSelected: false};
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
if (selectedImages.length < 11) {
|
||||
sendMessage({
|
||||
to: conversation.phone_num,
|
||||
body: message || "",
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
});
|
||||
setSelectedMedia(
|
||||
selectedMedia.map((i) => {
|
||||
return { ...i, isSelected: false };
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row" style={{width: "100%"}}>
|
||||
<ChatPresetsComponent className="imex-flex-row__margin"/>
|
||||
<ChatMediaSelector
|
||||
conversation={conversation}
|
||||
selectedMedia={selectedMedia}
|
||||
setSelectedMedia={setSelectedMedia}
|
||||
/>
|
||||
<span style={{flex: 1}}>
|
||||
return (
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
<ChatMediaSelector
|
||||
conversation={conversation}
|
||||
selectedMedia={selectedMedia}
|
||||
setSelectedMedia={setSelectedMedia}
|
||||
/>
|
||||
<span style={{ flex: 1 }}>
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
autoFocus
|
||||
ref={inputArea}
|
||||
autoSize={{minRows: 1, maxRows: 4}}
|
||||
value={message}
|
||||
disabled={isSending}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
autoFocus
|
||||
ref={inputArea}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={message}
|
||||
disabled={isSending}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<SendOutlined
|
||||
className="imex-flex-row__margin"
|
||||
// disabled={message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
<Spin
|
||||
style={{display: `${isSending ? "" : "none"}`}}
|
||||
indicator={
|
||||
<LoadingOutlined
|
||||
style={{
|
||||
fontSize: 24,
|
||||
}}
|
||||
spin
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<SendOutlined
|
||||
className="imex-flex-row__margin"
|
||||
// disabled={message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
indicator={
|
||||
<LoadingOutlined
|
||||
style={{
|
||||
fontSize: 24
|
||||
}}
|
||||
spin
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChatSendMessageComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatSendMessageComponent);
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import {CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons";
|
||||
import {Empty, Select, Space} from "antd";
|
||||
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Empty, Select, Space } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function ChatTagRoComponent({
|
||||
roOptions,
|
||||
loading,
|
||||
handleSearch,
|
||||
handleInsertTag,
|
||||
setOpen,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
export default function ChatTagRoComponent({ roOptions, loading, handleSearch, handleInsertTag, setOpen }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<div style={{width: "15rem"}}>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
popupMatchSelectWidth
|
||||
placeholder={t("general.labels.search")}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined/> : <Empty/>}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{loading ? <LoadingOutlined/> : null}
|
||||
return (
|
||||
<Space>
|
||||
<div style={{ width: "15rem" }}>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
popupMatchSelectWidth
|
||||
placeholder={t("general.labels.search")}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{loading ? <LoadingOutlined /> : null}
|
||||
|
||||
{loading ? (
|
||||
<LoadingOutlined/>
|
||||
) : (
|
||||
<CloseCircleOutlined onClick={() => setOpen(false)}/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
{loading ? <LoadingOutlined /> : <CloseCircleOutlined onClick={() => setOpen(false)} />}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,62 @@
|
||||
import {PlusOutlined} from "@ant-design/icons";
|
||||
import {useLazyQuery, useMutation} from "@apollo/client";
|
||||
import {Tag} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {INSERT_CONVERSATION_TAG} from "../../graphql/job-conversations.queries";
|
||||
import {SEARCH_FOR_JOBS} from "../../graphql/jobs.queries";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||
import ChatTagRo from "./chat-tag-ro.component";
|
||||
|
||||
export default function ChatTagRoContainer({conversation}) {
|
||||
const {t} = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
export default function ChatTagRoContainer({ conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [loadRo, {loading, data}] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
logImEXEvent("messaging_search_job_tag", {searchTerm: v});
|
||||
loadRo(v);
|
||||
};
|
||||
const executeSearch = (v) => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
||||
loadRo(v);
|
||||
};
|
||||
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch({variables: {search: value}});
|
||||
};
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
|
||||
variables: {conversationId: conversation.id},
|
||||
});
|
||||
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
|
||||
variables: { conversationId: conversation.id }
|
||||
});
|
||||
|
||||
const handleInsertTag = (value, option) => {
|
||||
logImEXEvent("messaging_add_job_tag");
|
||||
insertTag({variables: {jobId: option.key}});
|
||||
setOpen(false);
|
||||
};
|
||||
const handleInsertTag = (value, option) => {
|
||||
logImEXEvent("messaging_add_job_tag");
|
||||
insertTag({ variables: { jobId: option.key } });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const existingJobTags =
|
||||
conversation &&
|
||||
conversation.job_conversations &&
|
||||
conversation.job_conversations.map((i) => i.jobid);
|
||||
const existingJobTags =
|
||||
conversation && conversation.job_conversations && conversation.job_conversations.map((i) => i.jobid);
|
||||
|
||||
const roOptions = data
|
||||
? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
: [];
|
||||
const roOptions = data ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id)) : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{open ? (
|
||||
<ChatTagRo
|
||||
loading={loading}
|
||||
roOptions={roOptions}
|
||||
handleSearch={handleSearch}
|
||||
handleInsertTag={handleInsertTag}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
) : (
|
||||
<Tag onClick={() => setOpen(true)}>
|
||||
<PlusOutlined/>
|
||||
{t("messaging.actions.link")}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{open ? (
|
||||
<ChatTagRo
|
||||
loading={loading}
|
||||
roOptions={roOptions}
|
||||
handleSearch={handleSearch}
|
||||
handleInsertTag={handleInsertTag}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
) : (
|
||||
<Tag onClick={() => setOpen(true)}>
|
||||
<PlusOutlined />
|
||||
{t("messaging.actions.link")}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {Checkbox, Form} from "antd";
|
||||
import { Checkbox, Form } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
|
||||
const {name, label, required} = formItem;
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
valuePropName="checked"
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Checkbox disabled={readOnly}/>
|
||||
</Form.Item>
|
||||
);
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
valuePropName="checked"
|
||||
rules={[
|
||||
{
|
||||
required: required
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Checkbox disabled={readOnly} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from "react";
|
||||
import FormTypes from "./config-form-types";
|
||||
|
||||
export default function ConfirmFormComponents({componentList, readOnly}) {
|
||||
return (
|
||||
<div>
|
||||
{componentList.map((f, idx) => {
|
||||
const Comp = FormTypes[f.type];
|
||||
export default function ConfirmFormComponents({ componentList, readOnly }) {
|
||||
return (
|
||||
<div>
|
||||
{componentList.map((f, idx) => {
|
||||
const Comp = FormTypes[f.type];
|
||||
|
||||
if (!!Comp) {
|
||||
return <Comp key={idx} formItem={f} readOnly={readOnly}/>;
|
||||
} else {
|
||||
return <div key={idx}>Error</div>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
if (!!Comp) {
|
||||
return <Comp key={idx} formItem={f} readOnly={readOnly} />;
|
||||
} else {
|
||||
return <div key={idx}>Error</div>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import Text from "./text/text.component";
|
||||
import Textarea from "./textarea/textarea.component";
|
||||
|
||||
const e = {
|
||||
checkbox: CheckboxFormItem,
|
||||
slider: Slider,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
rate: Rate,
|
||||
checkbox: CheckboxFormItem,
|
||||
slider: Slider,
|
||||
text: Text,
|
||||
textarea: Textarea,
|
||||
rate: Rate
|
||||
};
|
||||
|
||||
export default e;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Form, Rate} from "antd";
|
||||
import { Form, Rate } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
|
||||
const {name, label, required} = formItem;
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Rate disabled={readOnly} allowHalf/>
|
||||
</Form.Item>
|
||||
);
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Rate disabled={readOnly} allowHalf />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Form, Slider} from "antd";
|
||||
import { Form, Slider } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
|
||||
const {name, label, required, min, max} = formItem;
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required, min, max } = formItem;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider disabled={readOnly} min={min || 0} max={max || 10}/>
|
||||
</Form.Item>
|
||||
);
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Slider disabled={readOnly} min={min || 0} max={max || 10} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {Form, Input} from "antd";
|
||||
import { Form, Input } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
|
||||
const {name, label, required} = formItem;
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={readOnly}/>
|
||||
</Form.Item>
|
||||
);
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required } = formItem;
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input disabled={readOnly} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {Form, Input} from "antd";
|
||||
import { Form, Input } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
|
||||
const {name, label, required, rows} = formItem;
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required, rows } = formItem;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea disabled={readOnly} rows={rows || 4}/>
|
||||
</Form.Item>
|
||||
);
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
label={label}
|
||||
rules={[
|
||||
{
|
||||
required: required
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.TextArea disabled={readOnly} rows={rows || 4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
import React from "react";
|
||||
import {Button, Result} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
||||
import { Button, Result } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function ConflictComponent() {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.instanceconflictitle")}
|
||||
extra={
|
||||
<div>
|
||||
<div>{t("general.labels.instanceconflictext",{app: InstanceRenderManager({imex:'$t(titles.imexonline)', rome: '$t(titles.romeonline)', promanager: '$t(titles.promanager)'})})}</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.instanceconflictitle")}
|
||||
extra={
|
||||
<div>
|
||||
<div>
|
||||
{t("general.labels.instanceconflictext", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)",
|
||||
promanager: "$t(titles.promanager)"
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,152 +1,127 @@
|
||||
import {Card, Input, Table} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import { Card, Input, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function ContractsCarsComponent({
|
||||
loading,
|
||||
data,
|
||||
selectedCarId,
|
||||
handleSelect,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {text: ""},
|
||||
search: "",
|
||||
});
|
||||
export default function ContractsCarsComponent({ loading, data, selectedCarId, handleSelect }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
search: ""
|
||||
});
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => <div>{t(record.status)}</div>,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness),
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "year" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "make" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.color"),
|
||||
dataIndex: "color",
|
||||
key: "color",
|
||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder: state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => <div>{t(record.status)}</div>
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
sortOrder: state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness)
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.color"),
|
||||
dataIndex: "color",
|
||||
key: "color",
|
||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
||||
sortOrder: state.sortedInfo.columnKey === "color" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
(cc) =>
|
||||
(cc.fleetnumber || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.status || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.year || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.make || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.model || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.color || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
);
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
(cc) =>
|
||||
(cc.fleetnumber || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.status || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.year || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.make || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.model || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.color || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("contracts.labels.availablecars")}
|
||||
extra={
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={(e) => setState({...state, search: e.target.value})}
|
||||
/>
|
||||
return (
|
||||
<Card
|
||||
title={t("contracts.labels.availablecars")}
|
||||
extra={
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={(e) => setState({ ...state, search: e.target.value })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCarId]
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleSelect(record);
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{position: "top"}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCarId],
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleSelect(record);
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {useQuery} from "@apollo/client";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import {QUERY_AVAILABLE_CC} from "../../graphql/courtesy-car.queries";
|
||||
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ContractCarsComponent from "./contract-cars.component";
|
||||
|
||||
export default function ContractCarsContainer({selectedCarState, form}) {
|
||||
const {loading, error, data} = useQuery(QUERY_AVAILABLE_CC, {
|
||||
variables: {today: dayjs().format("YYYY-MM-DD")},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
export default function ContractCarsContainer({ selectedCarState, form }) {
|
||||
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
|
||||
variables: { today: dayjs().format("YYYY-MM-DD") },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||
|
||||
const handleSelect = (record) => {
|
||||
setSelectedCar(record);
|
||||
|
||||
form.setFieldsValue({
|
||||
kmstart: record.mileage,
|
||||
dailyrate: record.dailycost,
|
||||
fuelout: record.fuel,
|
||||
damage: record.damage
|
||||
});
|
||||
};
|
||||
|
||||
const [selectedCar, setSelectedCar] = selectedCarState;
|
||||
|
||||
const handleSelect = (record) => {
|
||||
setSelectedCar(record);
|
||||
|
||||
form.setFieldsValue({
|
||||
kmstart: record.mileage,
|
||||
dailyrate: record.dailycost,
|
||||
fuelout: record.fuel,
|
||||
damage: record.damage,
|
||||
});
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
return (
|
||||
<ContractCarsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedCarId={selectedCar && selectedCar.id}
|
||||
loading={loading}
|
||||
data={data ? data.courtesycars : []}
|
||||
/>
|
||||
);
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<ContractCarsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedCarId={selectedCar && selectedCar.id}
|
||||
loading={loading}
|
||||
data={data ? data.courtesycars : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,397 +1,381 @@
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, Form, InputNumber, notification, Popover, Radio, Select, Space,} from "antd";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Form, InputNumber, notification, Popover, Radio, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_NEW_JOB} from "../../graphql/jobs.queries";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ContractConvertToRo({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
contract,
|
||||
disabled,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
||||
const history = useNavigate();
|
||||
export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
||||
const history = useNavigate();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
|
||||
const contractLength = dayjs(contract.actualreturn).diff(
|
||||
dayjs(contract.start),
|
||||
"day"
|
||||
);
|
||||
const billingLines = [];
|
||||
if (contractLength > 0)
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 1,
|
||||
line_no: 1,
|
||||
line_ref: 1,
|
||||
line_desc: t("contracts.fields.dailyrate"),
|
||||
db_price: contract.dailyrate,
|
||||
act_price: contract.dailyrate,
|
||||
part_qty: contractLength,
|
||||
part_type: "CCDR",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
db_ref: "io-ccdr",
|
||||
// mod_lbr_ty: "PAL",
|
||||
});
|
||||
const mileageDiff =
|
||||
contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
|
||||
if (mileageDiff > 0) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 2,
|
||||
line_no: 2,
|
||||
line_ref: 2,
|
||||
line_desc: "Fuel Surcharge",
|
||||
db_price: contract.excesskmrate,
|
||||
act_price: contract.excesskmrate,
|
||||
part_type: "CCM",
|
||||
part_qty: mileageDiff,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccm",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
const contractLength = dayjs(contract.actualreturn).diff(dayjs(contract.start), "day");
|
||||
const billingLines = [];
|
||||
if (contractLength > 0)
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 1,
|
||||
line_no: 1,
|
||||
line_ref: 1,
|
||||
line_desc: t("contracts.fields.dailyrate"),
|
||||
db_price: contract.dailyrate,
|
||||
act_price: contract.dailyrate,
|
||||
part_qty: contractLength,
|
||||
part_type: "CCDR",
|
||||
tax_part: true,
|
||||
mod_lb_hrs: 0,
|
||||
db_ref: "io-ccdr"
|
||||
// mod_lbr_ty: "PAL",
|
||||
});
|
||||
const mileageDiff = contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
|
||||
if (mileageDiff > 0) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 2,
|
||||
line_no: 2,
|
||||
line_ref: 2,
|
||||
line_desc: "Fuel Surcharge",
|
||||
db_price: contract.excesskmrate,
|
||||
act_price: contract.excesskmrate,
|
||||
part_type: "CCM",
|
||||
part_qty: mileageDiff,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccm",
|
||||
mod_lb_hrs: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (values.refuelqty > 0) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 3,
|
||||
line_no: 3,
|
||||
line_ref: 3,
|
||||
line_desc: t("contracts.fields.refuelcharge"),
|
||||
db_price: contract.refuelcharge,
|
||||
act_price: contract.refuelcharge,
|
||||
part_qty: values.refuelqty,
|
||||
part_type: "CCF",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccf",
|
||||
mod_lb_hrs: 0
|
||||
});
|
||||
}
|
||||
if (values.applyCleanupCharge) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 4,
|
||||
line_no: 4,
|
||||
line_ref: 4,
|
||||
line_desc: t("contracts.fields.cleanupcharge"),
|
||||
db_price: contract.cleanupcharge,
|
||||
act_price: contract.cleanupcharge,
|
||||
part_qty: 1,
|
||||
part_type: "CCC",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccc",
|
||||
mod_lb_hrs: 0
|
||||
});
|
||||
}
|
||||
if (contract.damagewaiver) {
|
||||
//Add for cleanup fee.
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 5,
|
||||
line_no: 5,
|
||||
line_ref: 5,
|
||||
line_desc: t("contracts.fields.damagewaiver"),
|
||||
db_price: contract.damagewaiver,
|
||||
act_price: contract.damagewaiver,
|
||||
part_type: "CCD",
|
||||
part_qty: 1,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccd",
|
||||
mod_lb_hrs: 0
|
||||
});
|
||||
}
|
||||
|
||||
const newJob = {
|
||||
// converted: true,
|
||||
shopid: bodyshop.id,
|
||||
ownerid: contract.job.ownerid,
|
||||
vehicleid: contract.job.vehicleid,
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||
ins_co_nm: values.ins_co_nm,
|
||||
class: values.class,
|
||||
converted: true,
|
||||
clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
|
||||
ownr_fn: contract.job.owner.ownr_fn,
|
||||
ownr_ln: contract.job.owner.ownr_ln,
|
||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||
ownr_ph1: contract.job.owner.ownr_ph1,
|
||||
ownr_ea: contract.job.owner.ownr_ea,
|
||||
v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
|
||||
v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
|
||||
v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc,
|
||||
v_vin: contract.job.vehicle && contract.job.vehicle.v_vin,
|
||||
status: bodyshop.md_ro_statuses.default_completed,
|
||||
notes: {
|
||||
data: [
|
||||
{
|
||||
text: t("contracts.labels.noteconvertedfrom", {
|
||||
agreementnumber: contract.agreementnumber
|
||||
}),
|
||||
audit: true,
|
||||
created_by: currentUser.email
|
||||
}
|
||||
]
|
||||
},
|
||||
joblines: {
|
||||
data: billingLines
|
||||
},
|
||||
parts_tax_rates: {
|
||||
PAA: {
|
||||
prt_type: "PAA",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAC: {
|
||||
prt_type: "PAC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAL: {
|
||||
prt_type: "PAL",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAM: {
|
||||
prt_type: "PAM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAN: {
|
||||
prt_type: "PAN",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAR: {
|
||||
prt_type: "PAR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
PAS: {
|
||||
prt_type: "PAS",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
CCDR: {
|
||||
prt_type: "CCDR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
CCF: {
|
||||
prt_type: "CCF",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
CCM: {
|
||||
prt_type: "CCM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
CCC: {
|
||||
prt_type: "CCC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
CCD: {
|
||||
prt_type: "CCD",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
}
|
||||
|
||||
if (values.refuelqty > 0) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 3,
|
||||
line_no: 3,
|
||||
line_ref: 3,
|
||||
line_desc: t("contracts.fields.refuelcharge"),
|
||||
db_price: contract.refuelcharge,
|
||||
act_price: contract.refuelcharge,
|
||||
part_qty: values.refuelqty,
|
||||
part_type: "CCF",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccf",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
if (values.applyCleanupCharge) {
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 4,
|
||||
line_no: 4,
|
||||
line_ref: 4,
|
||||
line_desc: t("contracts.fields.cleanupcharge"),
|
||||
db_price: contract.cleanupcharge,
|
||||
act_price: contract.cleanupcharge,
|
||||
part_qty: 1,
|
||||
part_type: "CCC",
|
||||
tax_part: true,
|
||||
db_ref: "io-ccc",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
if (contract.damagewaiver) {
|
||||
//Add for cleanup fee.
|
||||
billingLines.push({
|
||||
manual_line: true,
|
||||
unq_seq: 5,
|
||||
line_no: 5,
|
||||
line_ref: 5,
|
||||
line_desc: t("contracts.fields.damagewaiver"),
|
||||
db_price: contract.damagewaiver,
|
||||
act_price: contract.damagewaiver,
|
||||
part_type: "CCD",
|
||||
part_qty: 1,
|
||||
tax_part: true,
|
||||
db_ref: "io-ccd",
|
||||
mod_lb_hrs: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const newJob = {
|
||||
// converted: true,
|
||||
shopid: bodyshop.id,
|
||||
ownerid: contract.job.ownerid,
|
||||
vehicleid: contract.job.vehicleid,
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||
ins_co_nm: values.ins_co_nm,
|
||||
class: values.class,
|
||||
converted: true,
|
||||
clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
|
||||
ownr_fn: contract.job.owner.ownr_fn,
|
||||
ownr_ln: contract.job.owner.ownr_ln,
|
||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||
ownr_ph1: contract.job.owner.ownr_ph1,
|
||||
ownr_ea: contract.job.owner.ownr_ea,
|
||||
v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
|
||||
v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
|
||||
v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc,
|
||||
v_vin: contract.job.vehicle && contract.job.vehicle.v_vin,
|
||||
status: bodyshop.md_ro_statuses.default_completed,
|
||||
notes: {
|
||||
data: [
|
||||
{
|
||||
text: t("contracts.labels.noteconvertedfrom", {
|
||||
agreementnumber: contract.agreementnumber,
|
||||
}),
|
||||
audit: true,
|
||||
created_by: currentUser.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
joblines: {
|
||||
data: billingLines,
|
||||
},
|
||||
parts_tax_rates: {
|
||||
PAA: {
|
||||
prt_type: "PAA",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAC: {
|
||||
prt_type: "PAC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAL: {
|
||||
prt_type: "PAL",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAM: {
|
||||
prt_type: "PAM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAN: {
|
||||
prt_type: "PAN",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAR: {
|
||||
prt_type: "PAR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
PAS: {
|
||||
prt_type: "PAS",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCDR: {
|
||||
prt_type: "CCDR",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCF: {
|
||||
prt_type: "CCF",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCM: {
|
||||
prt_type: "CCM",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCC: {
|
||||
prt_type: "CCC",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
CCD: {
|
||||
prt_type: "CCD",
|
||||
prt_discp: 0,
|
||||
prt_mktyp: false,
|
||||
prt_mkupp: 0,
|
||||
prt_tax_in: true,
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
//Calcualte the new job totals.
|
||||
|
||||
const newTotals = (
|
||||
await axios.post("/job/totals", {
|
||||
job: {...newJob, joblines: billingLines},
|
||||
})
|
||||
).data;
|
||||
|
||||
newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
|
||||
newJob.job_totals = newTotals;
|
||||
|
||||
const result = await insertJob({
|
||||
variables: {job: [newJob]},
|
||||
// refetchQueries: ["GET_JOB_BY_PK"],
|
||||
// awaitRefetchQueries: true,
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.inserting", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.created"),
|
||||
onClick: () => {
|
||||
history.push(
|
||||
`/manage/jobs/${result.data.insert_jobs.returning[0].id}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
label={t("jobs.fields.ins_co_nm")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"class"}
|
||||
label={t("jobs.fields.class")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_class,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.applycleanupcharge")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={"applyCleanupCharge"}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>{t("general.labels.yes")}</Radio>
|
||||
<Radio value={false}>{t("general.labels.no")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.refuelqty")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={"refuelqty"}
|
||||
>
|
||||
<InputNumber precision={0} min={0}/>
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
<Button onClick={() => setOpen(false)}>
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
//Calcualte the new job totals.
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} open={open}>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
loading={loading}
|
||||
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
|
||||
>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
const newTotals = (
|
||||
await axios.post("/job/totals", {
|
||||
job: { ...newJob, joblines: billingLines }
|
||||
})
|
||||
).data;
|
||||
|
||||
newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
|
||||
newJob.job_totals = newTotals;
|
||||
|
||||
const result = await insertJob({
|
||||
variables: { job: [newJob] }
|
||||
// refetchQueries: ["GET_JOB_BY_PK"],
|
||||
// awaitRefetchQueries: true,
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.inserting", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.created"),
|
||||
onClick: () => {
|
||||
history.push(`/manage/jobs/${result.data.insert_jobs.returning[0].id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Form onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
label={t("jobs.fields.ins_co_nm")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"class"}
|
||||
label={t("jobs.fields.class")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_class
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.applycleanupcharge")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={"applyCleanupCharge"}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>{t("general.labels.yes")}</Radio>
|
||||
<Radio value={false}>{t("general.labels.no")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.labels.convertform.refuelqty")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={"refuelqty"}
|
||||
>
|
||||
<InputNumber precision={0} min={0} />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} open={open}>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
loading={loading}
|
||||
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
|
||||
>
|
||||
{t("contracts.actions.convertoro")}
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ContractConvertToRo);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ContractConvertToRo);
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
import {Card} from "antd";
|
||||
import { Card } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
|
||||
export default function ContractCourtesyCarBlock({courtesyCar}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
|
||||
<Card
|
||||
className="ant-card-grid-hoverable"
|
||||
style={{height: "100%"}}
|
||||
title={t("courtesycars.labels.courtesycar")}
|
||||
>
|
||||
<div>
|
||||
<DataLabel label={t("courtesycars.fields.fleetnumber")}>
|
||||
{(courtesyCar && courtesyCar.fleetnumber) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("courtesycars.fields.plate")}>
|
||||
{(courtesyCar && courtesyCar.plate) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("courtesycars.labels.vehicle")}>
|
||||
{`${(courtesyCar && courtesyCar.year) || ""} ${
|
||||
(courtesyCar && courtesyCar.make) || ""
|
||||
} ${(courtesyCar && courtesyCar.model) || ""}`}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
export default function ContractCourtesyCarBlock({ courtesyCar }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
|
||||
<Card className="ant-card-grid-hoverable" style={{ height: "100%" }} title={t("courtesycars.labels.courtesycar")}>
|
||||
<div>
|
||||
<DataLabel label={t("courtesycars.fields.fleetnumber")}>
|
||||
{(courtesyCar && courtesyCar.fleetnumber) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("courtesycars.fields.plate")}>{(courtesyCar && courtesyCar.plate) || ""}</DataLabel>
|
||||
<DataLabel label={t("courtesycars.labels.vehicle")}>
|
||||
{`${(courtesyCar && courtesyCar.year) || ""} ${
|
||||
(courtesyCar && courtesyCar.make) || ""
|
||||
} ${(courtesyCar && courtesyCar.model) || ""}`}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
import {useLazyQuery} from "@apollo/client";
|
||||
import {Button, notification} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {GET_JOB_FOR_CC_CONTRACT} from "../../graphql/jobs.queries";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GET_JOB_FOR_CC_CONTRACT } from "../../graphql/jobs.queries";
|
||||
|
||||
export default function ContractCreateJobPrefillComponent({jobId, form}) {
|
||||
const [call, {loading, error, data}] = useLazyQuery(
|
||||
GET_JOB_FOR_CC_CONTRACT
|
||||
);
|
||||
const {t} = useTranslation();
|
||||
export default function ContractCreateJobPrefillComponent({ jobId, form }) {
|
||||
const [call, { loading, error, data }] = useLazyQuery(GET_JOB_FOR_CC_CONTRACT);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
call({variables: {id: jobId}});
|
||||
};
|
||||
const handleClick = () => {
|
||||
call({ variables: { id: jobId } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
driver_dlst: data.jobs_by_pk.ownr_ast,
|
||||
driver_fn: data.jobs_by_pk.ownr_fn,
|
||||
driver_ln: data.jobs_by_pk.ownr_ln,
|
||||
driver_addr1: data.jobs_by_pk.ownr_addr1,
|
||||
driver_state: data.jobs_by_pk.ownr_st,
|
||||
driver_city: data.jobs_by_pk.ownr_city,
|
||||
driver_zip: data.jobs_by_pk.ownr_zip,
|
||||
driver_ph1: data.jobs_by_pk.ownr_ph1,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
if (error) {
|
||||
notification["error"]({
|
||||
message: t("contracts.errors.fetchingjobinfo", {
|
||||
error: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
driver_dlst: data.jobs_by_pk.ownr_ast,
|
||||
driver_fn: data.jobs_by_pk.ownr_fn,
|
||||
driver_ln: data.jobs_by_pk.ownr_ln,
|
||||
driver_addr1: data.jobs_by_pk.ownr_addr1,
|
||||
driver_state: data.jobs_by_pk.ownr_st,
|
||||
driver_city: data.jobs_by_pk.ownr_city,
|
||||
driver_zip: data.jobs_by_pk.ownr_zip,
|
||||
driver_ph1: data.jobs_by_pk.ownr_ph1
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} disabled={!jobId} loading={loading}>
|
||||
{t("contracts.labels.populatefromjob")}
|
||||
</Button>
|
||||
);
|
||||
if (error) {
|
||||
notification["error"]({
|
||||
message: t("contracts.errors.fetchingjobinfo", {
|
||||
error: JSON.stringify(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} disabled={!jobId} loading={loading}>
|
||||
{t("contracts.labels.populatefromjob")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {WarningFilled} from "@ant-design/icons";
|
||||
import {Form, Input, InputNumber, Space} from "antd";
|
||||
import { WarningFilled } from "@ant-design/icons";
|
||||
import { Form, Input, InputNumber, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
||||
@@ -11,361 +11,310 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import InputPhone, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
|
||||
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import ContractFormJobPrefill from "./contract-form-job-prefill.component";
|
||||
|
||||
export default function ContractFormComponent({
|
||||
form,
|
||||
create = false,
|
||||
selectedJobState,
|
||||
selectedCar,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form}/>
|
||||
<LayoutFormRow>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDateTimePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.scheduledreturn")}
|
||||
name="scheduledreturn"
|
||||
>
|
||||
<FormDateTimePicker/>
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
>
|
||||
<FormDateTimePicker/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{create && (
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
|
||||
>
|
||||
{() => {
|
||||
const insuranceOver =
|
||||
selectedCar &&
|
||||
selectedCar.insuranceexpires &&
|
||||
dayjs(selectedCar.insuranceexpires)
|
||||
.endOf("day")
|
||||
.isBefore(dayjs(form.getFieldValue("scheduledreturn")));
|
||||
if (insuranceOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
export default function ContractFormComponent({ form, create = false, selectedJobState, selectedCar }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form} />
|
||||
<LayoutFormRow>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ContractStatusSelector />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("contracts.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.scheduledreturn")} name="scheduledreturn">
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
)}
|
||||
{create && (
|
||||
<Form.Item shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}>
|
||||
{() => {
|
||||
const insuranceOver =
|
||||
selectedCar &&
|
||||
selectedCar.insuranceexpires &&
|
||||
dayjs(selectedCar.insuranceexpires)
|
||||
.endOf("day")
|
||||
.isBefore(dayjs(form.getFieldValue("scheduledreturn")));
|
||||
if (insuranceOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.insuranceexpired")}
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.insuranceexpired")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmstart")}
|
||||
name="kmstart"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber/>
|
||||
</Form.Item>
|
||||
{create && (
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.kmstart !== c.kmstart || p.scheduledreturn !== c.scheduledreturn
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const mileageOver =
|
||||
selectedCar && selectedCar.nextservicekm
|
||||
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
|
||||
: false;
|
||||
const dueForService =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicedate &&
|
||||
dayjs(selectedCar.nextservicedate)
|
||||
.endOf("day")
|
||||
.isSameOrBefore(
|
||||
dayjs(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
if (mileageOver || dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
</Space>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmstart")}
|
||||
name="kmstart"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{create && (
|
||||
<Form.Item shouldUpdate={(p, c) => p.kmstart !== c.kmstart || p.scheduledreturn !== c.scheduledreturn}>
|
||||
{() => {
|
||||
const mileageOver =
|
||||
selectedCar && selectedCar.nextservicekm
|
||||
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
|
||||
: false;
|
||||
const dueForService =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicedate &&
|
||||
dayjs(selectedCar.nextservicedate)
|
||||
.endOf("day")
|
||||
.isSameOrBefore(dayjs(form.getFieldValue("scheduledreturn")));
|
||||
if (mileageOver || dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
</span>
|
||||
<span>{`${
|
||||
selectedCar && selectedCar.nextservicekm
|
||||
} km`}</span>
|
||||
<span>
|
||||
<DateFormatter>
|
||||
{selectedCar && selectedCar.nextservicedate}
|
||||
</DateFormatter>
|
||||
<span>{`${selectedCar && selectedCar.nextservicekm} km`}</span>
|
||||
<span>
|
||||
<DateFormatter>{selectedCar && selectedCar.nextservicedate}</DateFormatter>
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={t("contracts.fields.damage")} name="damage">
|
||||
<Input.TextArea/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelout")}
|
||||
name="fuelout"
|
||||
span={8}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider/>
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelin")}
|
||||
name="fuelin"
|
||||
span={8}
|
||||
>
|
||||
<CourtesyCarFuelSlider/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={t("contracts.fields.damage")} name="damage">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelout")}
|
||||
name="fuelout"
|
||||
span={8}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
{create ? null : (
|
||||
<Form.Item label={t("contracts.fields.fuelin")} name="fuelin" span={8}>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<div>
|
||||
<Space wrap>
|
||||
{selectedJobState && (
|
||||
<div>
|
||||
<Space wrap>
|
||||
{selectedJobState && (
|
||||
<div>
|
||||
<ContractFormJobPrefill
|
||||
jobId={selectedJobState && selectedJobState[0]}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
//<ContractLicenseDecodeButton form={form} />
|
||||
}
|
||||
</Space>
|
||||
<ContractFormJobPrefill jobId={selectedJobState && selectedJobState[0]} form={form} />
|
||||
</div>
|
||||
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
||||
)}
|
||||
{
|
||||
//<ContractLicenseDecodeButton form={form} />
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
name="driver_dlnumber"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.driver_dlexpiry !== c.driver_dlexpiry || p.scheduledreturn !== c.scheduledreturn}
|
||||
>
|
||||
{() => {
|
||||
const dlExpiresBeforeReturn = dayjs(form.getFieldValue("driver_dlexpiry")).isBefore(
|
||||
dayjs(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlnumber")}
|
||||
name="driver_dlnumber"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.driver_dlexpiry !== c.driver_dlexpiry ||
|
||||
p.scheduledreturn !== c.scheduledreturn
|
||||
label={t("contracts.fields.driver_dlexpiry")}
|
||||
name="driver_dlexpiry"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
{() => {
|
||||
const dlExpiresBeforeReturn = dayjs(
|
||||
form.getFieldValue("driver_dlexpiry")
|
||||
).isBefore(dayjs(form.getFieldValue("scheduledreturn")));
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
{dlExpiresBeforeReturn && (
|
||||
<Space style={{ color: "tomato" }}>
|
||||
<WarningFilled />
|
||||
<span>{t("contracts.labels.dlexpirebeforereturn")}</span>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_dlexpiry")}
|
||||
name="driver_dlexpiry"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
{dlExpiresBeforeReturn && (
|
||||
<Space style={{color: "tomato"}}>
|
||||
<WarningFilled/>
|
||||
<span>{t("contracts.labels.dlexpirebeforereturn")}</span>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_fn")}
|
||||
name="driver_fn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ln")}
|
||||
name="driver_ln"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr1")}
|
||||
name="driver_addr1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr2")}
|
||||
name="driver_addr2"
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_city")} name="driver_city">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_state")}
|
||||
name="driver_state"
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_zip")} name="driver_zip">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ph1")}
|
||||
name="driver_ph1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({getFieldValue}) =>
|
||||
PhoneItemFormatterValidation(getFieldValue, "driver_ph1"),
|
||||
]}
|
||||
>
|
||||
<InputPhone/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<ContractsRatesChangeButton form={form}/>
|
||||
<LayoutFormRow header={t("contracts.labels.rates")}>
|
||||
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actax")} name="actax">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.dailyfreekm")} name="dailyfreekm">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.refuelcharge")}
|
||||
name="refuelcharge"
|
||||
>
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.excesskmrate")}
|
||||
name="excesskmrate"
|
||||
>
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cleanupcharge")}
|
||||
name="cleanupcharge"
|
||||
>
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.damagewaiver")}
|
||||
name="damagewaiver"
|
||||
>
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
||||
<InputNumber precision={2}/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
<Form.Item label={t("contracts.fields.driver_dlst")} name="driver_dlst">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_fn")}
|
||||
name="driver_fn"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ln")}
|
||||
name="driver_ln"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_addr1")}
|
||||
name="driver_addr1"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_city")} name="driver_city">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_state")} name="driver_state">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_zip")} name="driver_zip">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.driver_ph1")}
|
||||
name="driver_ph1"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "driver_ph1")
|
||||
]}
|
||||
>
|
||||
<InputPhone />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<ContractsRatesChangeButton form={form} />
|
||||
<LayoutFormRow header={t("contracts.labels.rates")}>
|
||||
<Form.Item label={t("contracts.fields.dailyrate")} name="dailyrate">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actax")} name="actax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.dailyfreekm")} name="dailyfreekm">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.refuelcharge")} name="refuelcharge">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.excesskmrate")} name="excesskmrate">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.cleanupcharge")} name="cleanupcharge">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("contracts.fields.damagewaiver")} name="damagewaiver">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.federaltax")} name="federaltax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.statetax")} name="statetax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.localtax")} name="localtax">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.coverage")} name="coverage">
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import {Card} from "antd";
|
||||
import { Card } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function ContractJobBlock({job}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/jobs/${job && job.id}`}>
|
||||
<Card
|
||||
className="ant-card-grid-hoverable"
|
||||
style={{height: "100%"}}
|
||||
title={t("jobs.labels.job")}
|
||||
>
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.ro_number")}>
|
||||
{(job && job.ro_number) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.vehicle")}>
|
||||
{`${(job && job.v_model_yr) || ""} ${
|
||||
(job && job.v_make_desc) || ""
|
||||
} ${(job && job.v_model_desc) || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.owner")}>
|
||||
<OwnerNameDisplay ownerObject={job}/>
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
export default function ContractJobBlock({ job }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Link to={`/manage/jobs/${job && job.id}`}>
|
||||
<Card className="ant-card-grid-hoverable" style={{ height: "100%" }} title={t("jobs.labels.job")}>
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.ro_number")}>{(job && job.ro_number) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.vehicle")}>
|
||||
{`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) || ""} ${(job && job.v_model_desc) || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.owner")}>
|
||||
<OwnerNameDisplay ownerObject={job} />
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,201 +1,155 @@
|
||||
import {Card, Input, Table} from "antd";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import { Card, Input, Table } from "antd";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
export default function ContractsJobsComponent({
|
||||
loading,
|
||||
data,
|
||||
selectedJob,
|
||||
handleSelect,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {text: ""},
|
||||
search: "",
|
||||
});
|
||||
export default function ContractsJobsComponent({ loading, data, selectedJob, handleSelect }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
search: ""
|
||||
});
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.ro_number ? record.ro_number : t("general.labels.na")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
width: "25%",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => <OwnerNameDisplay ownerObject={record}/>,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: "10%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
},
|
||||
},
|
||||
render: (text, record) => <span>{record.ro_number ? record.ro_number : t("general.labels.na")}</span>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
width: "25%",
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => <OwnerNameDisplay ownerObject={record} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: "10%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
width: "15%",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<span>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
) : (
|
||||
t("jobs.errors.novehicle")
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("vehicles.fields.plate_no"),
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
width: "8%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? (
|
||||
<span>{record.plate_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? (
|
||||
<span>{record.clm_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
width: "15%",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
) : (
|
||||
t("jobs.errors.novehicle")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("vehicles.fields.plate_no"),
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
width: "8%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? <span>{record.plate_no}</span> : t("general.labels.unknown");
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? <span>{record.clm_no}</span> : t("general.labels.unknown");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
(j) =>
|
||||
(j.ro_number || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_co_nm || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.clm_no || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase()) ||
|
||||
(j.plate_no || "")
|
||||
.toLowerCase()
|
||||
.includes(state.search.toLowerCase())
|
||||
);
|
||||
const filteredData =
|
||||
state.search === ""
|
||||
? data
|
||||
: data.filter(
|
||||
(j) =>
|
||||
(j.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
|
||||
(j.plate_no || "").toLowerCase().includes(state.search.toLowerCase())
|
||||
);
|
||||
|
||||
const defaultCurrent = useMemo(() => {
|
||||
const page =
|
||||
Math.floor(
|
||||
(filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10
|
||||
) + 1;
|
||||
if (page === 0) return 1;
|
||||
return page;
|
||||
}, [filteredData, selectedJob]);
|
||||
const defaultCurrent = useMemo(() => {
|
||||
const page = Math.floor((filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10) + 1;
|
||||
if (page === 0) return 1;
|
||||
return page;
|
||||
}, [filteredData, selectedJob]);
|
||||
|
||||
if (loading) return <LoadingSkeleton/>;
|
||||
return (
|
||||
<Card
|
||||
title={t("jobs.labels.availablejobs")}
|
||||
extra={
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={(e) => setState({...state, search: e.target.value})}
|
||||
/>
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
return (
|
||||
<Card
|
||||
title={t("jobs.labels.availablejobs")}
|
||||
extra={
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={state.search}
|
||||
onChange={(e) => setState({ ...state, search: e.target.value })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
defaultPageSize: pageLimit,
|
||||
defaultCurrent: defaultCurrent
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedJob]
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleSelect(record);
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
defaultPageSize: pageLimit,
|
||||
defaultCurrent: defaultCurrent,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: handleSelect,
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedJob],
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleSelect(record);
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import {useQuery} from "@apollo/client";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {QUERY_ALL_ACTIVE_JOBS} from "../../graphql/jobs.queries";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ContractJobsComponent from "./contract-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function ContractJobsContainer({selectedJobState, bodyshop}) {
|
||||
const {loading, error, data} = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||
variables: {
|
||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
||||
isConverted: true,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
const [selectedJob, setSelectedJob] = selectedJobState;
|
||||
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
||||
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||
variables: {
|
||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
||||
isConverted: true
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const [selectedJob, setSelectedJob] = selectedJobState;
|
||||
|
||||
const handleSelect = (record) => {
|
||||
setSelectedJob(record.id);
|
||||
};
|
||||
const handleSelect = (record) => {
|
||||
setSelectedJob(record.id);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
return (
|
||||
<ContractJobsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedJob={selectedJob}
|
||||
loading={loading}
|
||||
data={data ? data.jobs : []}
|
||||
/>
|
||||
);
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<ContractJobsComponent
|
||||
handleSelect={handleSelect}
|
||||
selectedJob={selectedJob}
|
||||
loading={loading}
|
||||
data={data ? data.jobs : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ContractJobsContainer);
|
||||
|
||||
@@ -1,123 +1,101 @@
|
||||
import {Button, Input, Modal, Typography} from "antd";
|
||||
import { Button, Input, Modal, Typography } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import aamva from "../../utils/aamva";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
export default function ContractLicenseDecodeButton({form}) {
|
||||
const {t} = useTranslation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [decodedBarcode, setDecodedBarcode] = useState(null);
|
||||
export default function ContractLicenseDecodeButton({ form }) {
|
||||
const { t } = useTranslation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [decodedBarcode, setDecodedBarcode] = useState(null);
|
||||
|
||||
const handleDecode = (e) => {
|
||||
logImEXEvent("contract_license_decode");
|
||||
setLoading(true);
|
||||
const aamvaParse = aamva.parse(e.currentTarget.value);
|
||||
setDecodedBarcode(aamvaParse);
|
||||
setLoading(false);
|
||||
const handleDecode = (e) => {
|
||||
logImEXEvent("contract_license_decode");
|
||||
setLoading(true);
|
||||
const aamvaParse = aamva.parse(e.currentTarget.value);
|
||||
setDecodedBarcode(aamvaParse);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleInsertForm = () => {
|
||||
logImEXEvent("contract_license_decode_fill_form");
|
||||
|
||||
const values = {
|
||||
driver_dlnumber: decodedBarcode.dl,
|
||||
driver_dlexpiry: dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`),
|
||||
driver_dlst: decodedBarcode.state,
|
||||
driver_fn: decodedBarcode.name.first,
|
||||
driver_ln: decodedBarcode.name.last,
|
||||
driver_addr1: decodedBarcode.address,
|
||||
driver_city: decodedBarcode.city,
|
||||
driver_state: decodedBarcode.state,
|
||||
driver_zip: decodedBarcode.postal_code,
|
||||
driver_dob: dayjs(decodedBarcode.birthday)
|
||||
};
|
||||
|
||||
const handleInsertForm = () => {
|
||||
logImEXEvent("contract_license_decode_fill_form");
|
||||
form.setFieldsValue(values);
|
||||
setModalVisible(false);
|
||||
setDecodedBarcode(null);
|
||||
};
|
||||
const handleClick = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const values = {
|
||||
driver_dlnumber: decodedBarcode.dl,
|
||||
driver_dlexpiry: dayjs(
|
||||
`20${decodedBarcode.expiration_date}${dayjs(
|
||||
decodedBarcode.birthday
|
||||
).format("DD")}`
|
||||
),
|
||||
driver_dlst: decodedBarcode.state,
|
||||
driver_fn: decodedBarcode.name.first,
|
||||
driver_ln: decodedBarcode.name.last,
|
||||
driver_addr1: decodedBarcode.address,
|
||||
driver_city: decodedBarcode.city,
|
||||
driver_state: decodedBarcode.state,
|
||||
driver_zip: decodedBarcode.postal_code,
|
||||
driver_dob: dayjs(decodedBarcode.birthday),
|
||||
};
|
||||
|
||||
form.setFieldsValue(values);
|
||||
setModalVisible(false);
|
||||
setDecodedBarcode(null);
|
||||
};
|
||||
const handleClick = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
open={modalVisible}
|
||||
okText={t("contracts.actions.senddltoform")}
|
||||
onOk={handleInsertForm}
|
||||
okButtonProps={{ disabled: !!!decodedBarcode }}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<div>
|
||||
<Modal
|
||||
open={modalVisible}
|
||||
okText={t("contracts.actions.senddltoform")}
|
||||
onOk={handleInsertForm}
|
||||
okButtonProps={{disabled: !!!decodedBarcode}}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
allowClear
|
||||
onChange={(e) => {
|
||||
if (!loading) setLoading(true);
|
||||
}}
|
||||
onPressEnter={handleDecode}
|
||||
/>
|
||||
</div>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
{decodedBarcode ? (
|
||||
<div>
|
||||
<DataLabel label={t("contracts.fields.driver_dlst")}>{decodedBarcode.state}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlnumber")}>{decodedBarcode.dl}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_fn")}>{decodedBarcode.name.first}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_ln")}>{decodedBarcode.name.last}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr1")}>{decodedBarcode.address}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr2")}>{decodedBarcode.address}</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlexpiry")}>
|
||||
{dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`).format(
|
||||
"MM/DD/YYYY"
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dob")}>
|
||||
{dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
|
||||
</DataLabel>
|
||||
<div>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
allowClear
|
||||
onChange={(e) => {
|
||||
if (!loading) setLoading(true);
|
||||
}}
|
||||
onPressEnter={handleDecode}
|
||||
/>
|
||||
</div>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
{decodedBarcode ? (
|
||||
<div>
|
||||
<DataLabel label={t("contracts.fields.driver_dlst")}>
|
||||
{decodedBarcode.state}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlnumber")}>
|
||||
{decodedBarcode.dl}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_fn")}>
|
||||
{decodedBarcode.name.first}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_ln")}>
|
||||
{decodedBarcode.name.last}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr1")}>
|
||||
{decodedBarcode.address}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_addr2")}>
|
||||
{decodedBarcode.address}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dlexpiry")}>
|
||||
{dayjs(
|
||||
`20${decodedBarcode.expiration_date}${dayjs(
|
||||
decodedBarcode.birthday
|
||||
).format("DD")}`
|
||||
).format("MM/DD/YYYY")}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("contracts.fields.driver_dob")}>
|
||||
{dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
|
||||
</DataLabel>
|
||||
<div>
|
||||
<Typography.Title level={4}>
|
||||
{t("contracts.labels.correctdataonform")}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("contracts.labels.waitingforscan")}</div>
|
||||
)}
|
||||
</LoadingSkeleton>
|
||||
<Typography.Title level={4}>{t("contracts.labels.correctdataonform")}</Typography.Title>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button onClick={handleClick}>
|
||||
{t("contracts.actions.decodelicense")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("contracts.labels.waitingforscan")}</div>
|
||||
)}
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
</Modal>
|
||||
<Button onClick={handleClick}>{t("contracts.actions.decodelicense")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import React, {forwardRef, useEffect, useState} from "react";
|
||||
import {Select} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, { forwardRef, useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
const { Option } = Select;
|
||||
|
||||
const ContractStatusComponent = ({value, onChange}, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const {t} = useTranslation();
|
||||
const ContractStatusComponent = ({ value, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={option}
|
||||
style={{
|
||||
width: 100,
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
|
||||
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
|
||||
<Option value="contracts.status.returned">
|
||||
{t("contracts.status.returned")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
|
||||
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
|
||||
<Option value="contracts.status.returned">{t("contracts.status.returned")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(ContractStatusComponent);
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {Form, Input} from "antd";
|
||||
import { Form, Input } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
|
||||
|
||||
export function PartsReceiveModalComponent({bodyshop, form}) {
|
||||
const {t} = useTranslation();
|
||||
export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="time"
|
||||
label={t("contracts.labels.time")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDateTimePicker/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="time"
|
||||
label={t("contracts.labels.time")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,169 +1,143 @@
|
||||
import {useLazyQuery} from "@apollo/client";
|
||||
import {Button, Form, Modal, Table} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {FIND_CONTRACT} from "../../graphql/cccontracts.queries";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectContractFinder} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Button, Form, Modal, Table } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { FIND_CONTRACT } from "../../graphql/cccontracts.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectContractFinder } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import ContractsFindModalComponent from "./contracts-find-modal.component";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
contractFinderModal: selectContractFinder,
|
||||
bodyshop: selectBodyshop,
|
||||
contractFinderModal: selectContractFinder
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder")),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder"))
|
||||
});
|
||||
|
||||
export function ContractsFindModalContainer({
|
||||
contractFinderModal,
|
||||
toggleModalVisible,
|
||||
contractFinderModal,
|
||||
toggleModalVisible,
|
||||
|
||||
bodyshop,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
bodyshop
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {open} = contractFinderModal;
|
||||
const { open } = contractFinderModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [callSearch, {loading, error, data}] = useLazyQuery(FIND_CONTRACT);
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("contract_finder_search");
|
||||
// const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(FIND_CONTRACT);
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("contract_finder_search");
|
||||
|
||||
//Execute contract find
|
||||
//Execute contract find
|
||||
|
||||
callSearch({
|
||||
variables: {
|
||||
plate:
|
||||
(values.plate && values.plate !== "" && values.plate) || undefined,
|
||||
time: values.time,
|
||||
callSearch({
|
||||
variables: {
|
||||
plate: (values.plate && values.plate !== "" && values.plate) || undefined,
|
||||
time: values.time
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width="70%"
|
||||
title={t("contracts.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||
<ContractsFindModalComponent form={form} />
|
||||
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
||||
{t("general.labels.search")}
|
||||
</Button>
|
||||
{error && <AlertComponent type="error" message={JSON.stringify(error)} />}
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={[
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>{record.agreementnumber || ""}</Link>
|
||||
)
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
width="70%"
|
||||
title={t("contracts.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onOk={() => toggleModalVisible()}
|
||||
destroyOnClose
|
||||
forceRender
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="no"
|
||||
onFinish={handleFinish}
|
||||
>
|
||||
<ContractsFindModalComponent form={form}/>
|
||||
<Button onClick={() => form.submit()} type="primary" loading={loading}>
|
||||
{t("general.labels.search")}
|
||||
</Button>
|
||||
{error && (
|
||||
<AlertComponent type="error" message={JSON.stringify(error)}/>
|
||||
)}
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={[
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.labels.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model} ${
|
||||
record.courtesycar.plate
|
||||
? `(${record.courtesycar.plate})`
|
||||
: ""
|
||||
}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
render: (text, record) => t(record.status),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.start}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.actualreturn"),
|
||||
dataIndex: "actualreturn",
|
||||
key: "actualreturn",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rowKey="id"
|
||||
dataSource={data && data.cccontracts}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number || ""}</Link>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
render: (text, record) => `${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
},
|
||||
{
|
||||
title: t("contracts.labels.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model} ${
|
||||
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
|
||||
}`}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
render: (text, record) => <DateTimeFormatter>{record.start}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
render: (text, record) => <DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.actualreturn"),
|
||||
dataIndex: "actualreturn",
|
||||
key: "actualreturn",
|
||||
render: (text, record) => <DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
|
||||
}
|
||||
]}
|
||||
rowKey="id"
|
||||
dataSource={data && data.cccontracts}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ContractsFindModalContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
|
||||
|
||||
@@ -1,224 +1,188 @@
|
||||
import {SyncOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Input, Space, Table, Typography} from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link, useLocation, useNavigate} from "react-router-dom";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
|
||||
|
||||
import dayjs from "../../utils/day";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setContractFinderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "contractFinder"})),
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
setContractFinderContext: (context) => dispatch(setModalContext({ context: context, modal: "contractFinder" }))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ContractsList);
|
||||
|
||||
export function ContractsList({
|
||||
bodyshop,
|
||||
loading,
|
||||
contracts,
|
||||
refetch,
|
||||
total,
|
||||
setContractFinderContext,
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {text: ""},
|
||||
});
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const {page} = search;
|
||||
export function ContractsList({ bodyshop, loading, contracts, refetch, total, setContractFinderContext }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
const history = useNavigate();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page } = search;
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "agreementnumber" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
bodyshop.last_name_first
|
||||
? `${record.driver_ln || ""}, ${record.driver_fn || ""}`
|
||||
: `${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.labels.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
//sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
//sortOrder:
|
||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model}${
|
||||
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
|
||||
}${
|
||||
record.courtesycar.fleetnumber
|
||||
? ` (${record.courtesycar.fleetnumber})`
|
||||
: ""
|
||||
}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.start}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduledreturn" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.actualreturn"),
|
||||
dataIndex: "actualreturn",
|
||||
key: "actualreturn",
|
||||
sorter: (a, b) => alphaSort(a.actualreturn, b.actualreturn),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actualreturn" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.length"),
|
||||
dataIndex: "length",
|
||||
key: "length",
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder: state.sortedInfo.columnKey === "agreementnumber" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>{record.agreementnumber || ""}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number || ""}</Link>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
|
||||
sortOrder: state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
bodyshop.last_name_first
|
||||
? `${record.driver_ln || ""}, ${record.driver_fn || ""}`
|
||||
: `${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
},
|
||||
{
|
||||
title: t("contracts.labels.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
//sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
//sortOrder:
|
||||
// state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model}${
|
||||
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
|
||||
}${record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""}`}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder: state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateTimeFormatter>{record.start}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
|
||||
sortOrder: state.sortedInfo.columnKey === "scheduledreturn" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateTimeFormatter>{record.scheduledreturn}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.actualreturn"),
|
||||
dataIndex: "actualreturn",
|
||||
key: "actualreturn",
|
||||
sorter: (a, b) => alphaSort(a.actualreturn, b.actualreturn),
|
||||
sortOrder: state.sortedInfo.columnKey === "actualreturn" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.length"),
|
||||
dataIndex: "length",
|
||||
key: "length",
|
||||
|
||||
render: (text, record) =>
|
||||
(record.actualreturn &&
|
||||
record.start &&
|
||||
`${dayjs(record.actualreturn)
|
||||
.diff(dayjs(record.start), "day", true)
|
||||
.toFixed(1)} days`) ||
|
||||
"",
|
||||
},
|
||||
];
|
||||
render: (text, record) =>
|
||||
(record.actualreturn &&
|
||||
record.start &&
|
||||
`${dayjs(record.actualreturn).diff(dayjs(record.start), "day", true).toFixed(1)} days`) ||
|
||||
""
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history({search: queryString.stringify(search)});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.searchresults", {search: search.search})}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
onClick={() => {
|
||||
delete search.search;
|
||||
history({search: queryString.stringify(search)});
|
||||
}}
|
||||
>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setContractFinderContext()}>
|
||||
{t("contracts.actions.find")}
|
||||
</Button>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined/>
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder={search.searh || t("general.labels.search")}
|
||||
onSearch={(value) => {
|
||||
search.search = value;
|
||||
history({search: queryString.stringify(search)});
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<ContractsFindModalContainer/>
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{
|
||||
x: "50%", //y: "40rem"
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.searchresults", { search: search.search })}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
onClick={() => {
|
||||
delete search.search;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setContractFinderContext()}>{t("contracts.actions.find")}</Button>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder={search.searh || t("general.labels.search")}
|
||||
onSearch={(value) => {
|
||||
search.search = value;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<ContractsFindModalContainer />
|
||||
<Table
|
||||
loading={loading}
|
||||
scroll={{
|
||||
x: "50%" //y: "40rem"
|
||||
}}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import {DownOutlined} from "@ant-design/icons";
|
||||
import {Dropdown} from "antd";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function ContractsRatesChangeButton({disabled, form, bodyshop}) {
|
||||
const {t} = useTranslation();
|
||||
export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = ({item, key, keyPath}) => {
|
||||
const {label, ...rate} = item.props.value;
|
||||
form.setFieldsValue(rate);
|
||||
};
|
||||
const handleClick = ({ item, key, keyPath }) => {
|
||||
const { label, ...rate } = item.props.value;
|
||||
form.setFieldsValue(rate);
|
||||
};
|
||||
|
||||
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: i.label,
|
||||
value: i,
|
||||
}));
|
||||
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: i.label,
|
||||
value: i
|
||||
}));
|
||||
|
||||
const menu = {items: menuItems, onClick: handleClick};
|
||||
const menu = { items: menuItems, onClick: handleClick };
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={disabled}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("contracts.actions.changerate")} <DownOutlined/>
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={disabled}>
|
||||
<a className="ant-dropdown-link" href=" #" onClick={(e) => e.preventDefault()}>
|
||||
{t("contracts.actions.changerate")} <DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ContractsRatesChangeButton);
|
||||
|
||||
@@ -1,104 +1,92 @@
|
||||
import {Card, Table} from "antd";
|
||||
import { Card, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link, useLocation, useNavigate} from "react-router-dom";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
export default function CourtesyCarContractListComponent({
|
||||
contracts,
|
||||
totalContracts,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const {page, sortcolumn, sortorder} = search;
|
||||
const history = useNavigate();
|
||||
export default function CourtesyCarContractListComponent({ contracts, totalContracts }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder } = search;
|
||||
const history = useNavigate();
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder: sortcolumn === "agreementnumber" && sortorder,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
|
||||
{record.agreementnumber || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
const columns = [
|
||||
{
|
||||
title: t("contracts.fields.agreementnumber"),
|
||||
dataIndex: "agreementnumber",
|
||||
key: "agreementnumber",
|
||||
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
|
||||
sortOrder: sortcolumn === "agreementnumber" && sortorder,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/contracts/${record.id}`}>{record.agreementnumber || ""}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "job.ro_number",
|
||||
key: "job.ro_number",
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
{record.job.ro_number || ""}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
render: (text, record) => <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number || ""}</Link>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.driver"),
|
||||
dataIndex: "driver_ln",
|
||||
key: "driver_ln",
|
||||
|
||||
render: (text, record) =>
|
||||
`${record.driver_fn || ""} ${record.driver_ln || ""}`,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => t(record.status),
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder: sortcolumn === "start" && sortorder,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
|
||||
sortOrder: sortcolumn === "scheduledreturn" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
),
|
||||
},
|
||||
];
|
||||
render: (text, record) => `${record.driver_fn || ""} ${record.driver_ln || ""}`
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => t(record.status)
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.start"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
sorter: (a, b) => alphaSort(a.start, b.start),
|
||||
sortOrder: sortcolumn === "start" && sortorder,
|
||||
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
|
||||
sortOrder: sortcolumn === "scheduledreturn" && sortorder,
|
||||
render: (text, record) => <DateFormatter>{record.scheduledreturn}</DateFormatter>
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history({search: queryString.stringify(search)});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("menus.header.courtesycars-contracts")}>
|
||||
<Table
|
||||
scroll={{x: true}}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: totalContracts,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("menus.header.courtesycars-contracts")}>
|
||||
<Table
|
||||
scroll={{ x: true }}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: totalContracts
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={contracts}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {WarningFilled} from "@ant-design/icons";
|
||||
import {useApolloClient} from "@apollo/client";
|
||||
import {Button, Form, Input, InputNumber, Space} from "antd";
|
||||
import {PageHeader} from "@ant-design/pro-layout";
|
||||
import { WarningFilled } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button, Form, Input, InputNumber, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {CHECK_CC_FLEET_NUMBER} from "../../graphql/courtesy-car.queries";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||
@@ -15,352 +15,306 @@ import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
export default function CourtesyCarCreateFormComponent({form, saveLoading}) {
|
||||
const {t} = useTranslation();
|
||||
const client = useApolloClient();
|
||||
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={t("menus.header.courtesycars")}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
loading={saveLoading}
|
||||
onClick={() => form.submit()}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={t("menus.header.courtesycars")}
|
||||
extra={
|
||||
<Button type="primary" loading={saveLoading} onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* <FormFieldsChanged form={form} /> */}
|
||||
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
name="year"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.model")}
|
||||
name="model"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.plate")}
|
||||
name="plate"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.color")}
|
||||
name="color"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.vin")}
|
||||
name="vin"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("courtesycars.labels.usage")}>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.mileage")}
|
||||
name="mileage"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fleetnumber")}
|
||||
name="fleetnumber"
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_CC_FLEET_NUMBER,
|
||||
variables: {
|
||||
name: value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.courtesycars_aggregate.aggregate.count === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.courtesycars_aggregate.nodes.length === 1 &&
|
||||
response.data.courtesycars_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("courtesycars.labels.uniquefleet"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
/>
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
{/* <FormFieldsChanged form={form} /> */}
|
||||
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.year")}
|
||||
name="year"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.make")}
|
||||
name="make"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.model")}
|
||||
name="model"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.plate")}
|
||||
name="plate"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.color")}
|
||||
name="color"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.vin")}
|
||||
name="vin"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("courtesycars.labels.usage")}>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.mileage")}
|
||||
name="mileage"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.fleetnumber")}
|
||||
name="fleetnumber"
|
||||
validateTrigger="onBlur"
|
||||
hasFeedback
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({getFieldValue}) => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const response = await client.query({
|
||||
query: CHECK_CC_FLEET_NUMBER,
|
||||
variables: {
|
||||
name: value,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
response.data.courtesycars_aggregate.aggregate.count === 0
|
||||
) {
|
||||
return Promise.resolve();
|
||||
} else if (
|
||||
response.data.courtesycars_aggregate.nodes.length === 1 &&
|
||||
response.data.courtesycars_aggregate.nodes[0].id ===
|
||||
form.getFieldValue("id")
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("courtesycars.labels.uniquefleet"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.purchasedate")}
|
||||
name="purchasedate"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.servicestartdate")}
|
||||
name="servicestartdate"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.serviceenddate")}
|
||||
name="serviceenddate"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.leaseenddate")}
|
||||
name="leaseenddate"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("courtesycars.labels.status")}>
|
||||
<Form.Item
|
||||
span={24}
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name="fuel"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
|
||||
<CourtesyCarReadiness/>
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicekm")}
|
||||
name="nextservicekm"
|
||||
>
|
||||
<InputNumber min={0} precision={0}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const nextservicekm = form.getFieldValue("nextservicekm");
|
||||
const mileageOver = nextservicekm
|
||||
? nextservicekm <= form.getFieldValue("mileage")
|
||||
: false;
|
||||
if (mileageOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
<LayoutFormRow header={t("courtesycars.labels.status")}>
|
||||
<Form.Item
|
||||
span={24}
|
||||
label={t("courtesycars.fields.fuel")}
|
||||
name="fuel"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.status")}
|
||||
name="status"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
|
||||
<CourtesyCarReadiness />
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item label={t("courtesycars.fields.nextservicekm")} name="nextservicekm">
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate={(p, c) => p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm}>
|
||||
{() => {
|
||||
const nextservicekm = form.getFieldValue("nextservicekm");
|
||||
const mileageOver = nextservicekm ? nextservicekm <= form.getFieldValue("mileage") : false;
|
||||
if (mileageOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
</span>
|
||||
<span>{`${nextservicekm} km`}</span>
|
||||
</Space>
|
||||
);
|
||||
<span>{`${nextservicekm} km`}</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicedate")}
|
||||
name="nextservicedate"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}
|
||||
>
|
||||
{() => {
|
||||
const nextservicedate = form.getFieldValue("nextservicedate");
|
||||
const dueForService =
|
||||
nextservicedate &&
|
||||
dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}>
|
||||
{() => {
|
||||
const nextservicedate = form.getFieldValue("nextservicedate");
|
||||
const dueForService = nextservicedate && dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
|
||||
|
||||
if (dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
if (dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.cardueforservice")}
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
<DateFormatter>{nextservicedate}</DateFormatter>
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input.TextArea/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input.TextArea/>
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.registrationexpires !== c.registrationexpires
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("registrationexpires");
|
||||
|
||||
const dateover =
|
||||
expires && dayjs(expires).endOf("day").isBefore(dayjs());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}
|
||||
>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("insuranceexpires");
|
||||
|
||||
const dateover =
|
||||
expires && dayjs(expires).endOf("day").isBefore(dayjs());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{color: "tomato"}}>
|
||||
<span>
|
||||
<WarningFilled style={{marginRight: ".3rem"}}/>
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires">
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("registrationexpires");
|
||||
|
||||
const dateover = expires && dayjs(expires).endOf("day").isBefore(dayjs());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("insuranceexpires");
|
||||
|
||||
const dateover = expires && dayjs(expires).endOf("day").isBefore(dayjs());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
import {Slider} from "antd";
|
||||
import React, {forwardRef} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Slider } from "antd";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CourtesyCarFuelComponent = (props, ref) => {
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const marks = {
|
||||
0: {
|
||||
style: {
|
||||
color: "#f50",
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.empty")}</strong>,
|
||||
},
|
||||
13: t("courtesycars.labels.fuel.18"),
|
||||
25: t("courtesycars.labels.fuel.14"),
|
||||
38: t("courtesycars.labels.fuel.38"),
|
||||
50: t("courtesycars.labels.fuel.12"),
|
||||
63: t("courtesycars.labels.fuel.58"),
|
||||
75: t("courtesycars.labels.fuel.34"),
|
||||
88: t("courtesycars.labels.fuel.78"),
|
||||
100: {
|
||||
style: {
|
||||
color: "#008000",
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>,
|
||||
},
|
||||
};
|
||||
const marks = {
|
||||
0: {
|
||||
style: {
|
||||
color: "#f50"
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.empty")}</strong>
|
||||
},
|
||||
13: t("courtesycars.labels.fuel.18"),
|
||||
25: t("courtesycars.labels.fuel.14"),
|
||||
38: t("courtesycars.labels.fuel.38"),
|
||||
50: t("courtesycars.labels.fuel.12"),
|
||||
63: t("courtesycars.labels.fuel.58"),
|
||||
75: t("courtesycars.labels.fuel.34"),
|
||||
88: t("courtesycars.labels.fuel.78"),
|
||||
100: {
|
||||
style: {
|
||||
color: "#008000"
|
||||
},
|
||||
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Slider
|
||||
ref={ref}
|
||||
marks={marks}
|
||||
step={null}
|
||||
style={{marginLeft: "2rem", marginRight: "2rem"}}
|
||||
{...props}
|
||||
tooltip={{
|
||||
formatter: (value) => {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.38");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 63:
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 75:
|
||||
return t("courtesycars.labels.fuel.34");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Slider
|
||||
ref={ref}
|
||||
marks={marks}
|
||||
step={null}
|
||||
style={{ marginLeft: "2rem", marginRight: "2rem" }}
|
||||
{...props}
|
||||
tooltip={{
|
||||
formatter: (value) => {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.38");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 63:
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 75:
|
||||
return t("courtesycars.labels.fuel.34");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default forwardRef(CourtesyCarFuelComponent);
|
||||
|
||||
@@ -1,36 +1,32 @@
|
||||
import {Select} from "antd";
|
||||
import React, {forwardRef, useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Select } from "antd";
|
||||
import React, { forwardRef, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarReadinessComponent = ({value, onChange}, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const {t} = useTranslation();
|
||||
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
allowClear
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{
|
||||
width: 100,
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.readiness.ready">
|
||||
{t("courtesycars.readiness.ready")}
|
||||
</Option>
|
||||
<Option value="courtesycars.readiness.notready">
|
||||
{t("courtesycars.readiness.notready")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
allowClear
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option>
|
||||
<Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(CourtesyCarReadinessComponent);
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import {Form, InputNumber} from "antd";
|
||||
import { Form, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
|
||||
export default function CourtesyCarReturnModalComponent() {
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmend")}
|
||||
name="kmend"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={0}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelin")}
|
||||
name={"fuelin"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actualreturn")}
|
||||
name="actualreturn"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.kmend")}
|
||||
name="kmend"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.fuelin")}
|
||||
name={"fuelin"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CourtesyCarFuelSlider />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,88 +1,77 @@
|
||||
import {Form, Modal, notification} from "antd";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectCourtesyCarReturn} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
|
||||
import dayjs from "../../utils/day";
|
||||
import {RETURN_CONTRACT} from "../../graphql/cccontracts.queries";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
|
||||
import { useMutation } from "@apollo/client";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
courtesyCarReturnModal: selectCourtesyCarReturn,
|
||||
bodyshop: selectBodyshop,
|
||||
courtesyCarReturnModal: selectCourtesyCarReturn,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn")),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn"))
|
||||
});
|
||||
|
||||
export function CCReturnModalContainer({
|
||||
courtesyCarReturnModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {open, context, actions} = courtesyCarReturnModal;
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [updateContract] = useMutation(RETURN_CONTRACT);
|
||||
const handleFinish = (values) => {
|
||||
setLoading(true);
|
||||
updateContract({
|
||||
variables: {
|
||||
contractId: context.contractId,
|
||||
cccontract: {
|
||||
kmend: values.kmend,
|
||||
actualreturn: values.actualreturn,
|
||||
status: "contracts.status.returned",
|
||||
fuelin: values.fuelin,
|
||||
},
|
||||
courtesycarid: context.courtesyCarId,
|
||||
courtesycar: {
|
||||
status: "courtesycars.status.in",
|
||||
fuel: values.fuelin,
|
||||
mileage: values.kmend,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
if (actions.refetch) actions.refetch();
|
||||
toggleModalVisible();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("contracts.errors.returning", {error: error}),
|
||||
});
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
export function CCReturnModalContainer({ courtesyCarReturnModal, toggleModalVisible, bodyshop }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { open, context, actions } = courtesyCarReturnModal;
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [updateContract] = useMutation(RETURN_CONTRACT);
|
||||
const handleFinish = (values) => {
|
||||
setLoading(true);
|
||||
updateContract({
|
||||
variables: {
|
||||
contractId: context.contractId,
|
||||
cccontract: {
|
||||
kmend: values.kmend,
|
||||
actualreturn: values.actualreturn,
|
||||
status: "contracts.status.returned",
|
||||
fuelin: values.fuelin
|
||||
},
|
||||
courtesycarid: context.courtesyCarId,
|
||||
courtesycar: {
|
||||
status: "courtesycars.status.in",
|
||||
fuel: values.fuelin,
|
||||
mileage: values.kmend
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => {
|
||||
if (actions.refetch) actions.refetch();
|
||||
toggleModalVisible();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("contracts.errors.returning", { error: error })
|
||||
});
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("courtesycars.labels.return")}
|
||||
open={open}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
width={"90%"}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{htmlType: "submit", loading: loading}}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={{fuel: 100, actualreturn: dayjs(new Date())}}
|
||||
>
|
||||
<CourtesyCarReturnModalComponent/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
title={t("courtesycars.labels.return")}
|
||||
open={open}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
width={"90%"}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ htmlType: "submit", loading: loading }}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish} initialValues={{ fuel: 100, actualreturn: dayjs(new Date()) }}>
|
||||
<CourtesyCarReturnModalComponent />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CCReturnModalContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CCReturnModalContainer);
|
||||
|
||||
@@ -1,44 +1,34 @@
|
||||
import React, {forwardRef, useEffect, useState} from "react";
|
||||
import {Select} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, { forwardRef, useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const {Option} = Select;
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarStatusComponent = ({value, onChange}, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const {t} = useTranslation();
|
||||
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{
|
||||
width: 100,
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.status.in">
|
||||
{t("courtesycars.status.in")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.inservice">
|
||||
{t("courtesycars.status.inservice")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.out">
|
||||
{t("courtesycars.status.out")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.sold">
|
||||
{t("courtesycars.status.sold")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.leasereturn">
|
||||
{t("courtesycars.status.leasereturn")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{
|
||||
width: 100
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.status.in">{t("courtesycars.status.in")}</Option>
|
||||
<Option value="courtesycars.status.inservice">{t("courtesycars.status.inservice")}</Option>
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(CourtesyCarStatusComponent);
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Input,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { Button, Card, Dropdown, Input, Space, Table, Tooltip } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,297 +12,270 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [filter, setFilter] = useLocalStorage(
|
||||
"filter_courtesy_cars_list",
|
||||
null
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [filter, setFilter] = useLocalStorage("filter_courtesy_cars_list", null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
const columns = [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder: state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.vin"),
|
||||
dataIndex: "vin",
|
||||
key: "vin",
|
||||
sorter: (a, b) => alphaSort(a.vin, b.vin),
|
||||
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
|
||||
render: (text, record) => <Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
filteredValue: filter?.status || null,
|
||||
filters: [
|
||||
{
|
||||
title: t("courtesycars.fields.fleetnumber"),
|
||||
dataIndex: "fleetnumber",
|
||||
key: "fleetnumber",
|
||||
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order,
|
||||
text: t("courtesycars.status.in"),
|
||||
value: "courtesycars.status.in"
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.vin"),
|
||||
dataIndex: "vin",
|
||||
key: "vin",
|
||||
sorter: (a, b) => alphaSort(a.vin, b.vin),
|
||||
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
filteredValue: filter?.status || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.status.in"),
|
||||
value: "courtesycars.status.in",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.inservice"),
|
||||
value: "courtesycars.status.inservice",
|
||||
value: "courtesycars.status.inservice"
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.out"),
|
||||
value: "courtesycars.status.out",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.sold"),
|
||||
value: "courtesycars.status.sold",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
|
||||
record;
|
||||
text: t("courtesycars.status.out"),
|
||||
value: "courtesycars.status.out"
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.sold"),
|
||||
value: "courtesycars.status.sold"
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn"
|
||||
}
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } = record;
|
||||
|
||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
||||
|
||||
const dueForService =
|
||||
nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs());
|
||||
const dueForService = nextservicedate && dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs());
|
||||
|
||||
const insuranceOver =
|
||||
insuranceexpires &&
|
||||
dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
|
||||
const insuranceOver = insuranceexpires && dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService || insuranceOver) && (
|
||||
<Tooltip
|
||||
title={
|
||||
(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") +
|
||||
" / " +
|
||||
t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}
|
||||
>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService || insuranceOver) && (
|
||||
<Tooltip
|
||||
title={
|
||||
(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") + " / " + t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}
|
||||
>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
value: "courtesycars.readiness.ready"
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
value: "courtesycars.readiness.ready",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.readiness.notready"),
|
||||
value: "courtesycars.readiness.notready",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.readiness),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness),
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "year" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "make" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.color"),
|
||||
dataIndex: "color",
|
||||
key: "color",
|
||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.fuel"),
|
||||
dataIndex: "fuel",
|
||||
key: "fuel",
|
||||
sorter: (a, b) => a.fuel - b.fuel,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
switch (record.fuel) {
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 75:
|
||||
text: t("courtesycars.readiness.notready"),
|
||||
value: "courtesycars.readiness.notready"
|
||||
}
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.readiness),
|
||||
sortOrder: state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness)
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
key: "year",
|
||||
sorter: (a, b) => alphaSort(a.year, b.year),
|
||||
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.make"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
sorter: (a, b) => alphaSort(a.make, b.make),
|
||||
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.model"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.color"),
|
||||
dataIndex: "color",
|
||||
key: "color",
|
||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
||||
sortOrder: state.sortedInfo.columnKey === "color" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.plate"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
sorter: (a, b) => alphaSort(a.plate, b.plate),
|
||||
sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.fuel"),
|
||||
dataIndex: "fuel",
|
||||
key: "fuel",
|
||||
sorter: (a, b) => a.fuel - b.fuel,
|
||||
sortOrder: state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
switch (record.fuel) {
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 75:
|
||||
return t("courtesycars.labels.fuel.34");
|
||||
case 63:
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.38");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
default:
|
||||
return record.fuel;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.labels.outwith"),
|
||||
dataIndex: "outwith",
|
||||
key: "outwith",
|
||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 ? (
|
||||
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
|
||||
{`${
|
||||
record.cccontracts[0].job.ro_number
|
||||
} - ${OwnerNameDisplayFunction(record.cccontracts[0].job)}`}
|
||||
</Link>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 && (
|
||||
<DateTimeFormatter>
|
||||
{record.cccontracts[0].scheduledreturn}
|
||||
</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const tableData = searchText
|
||||
? courtesycars.filter(
|
||||
(c) =>
|
||||
(c.fleetnumber || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(c.vin || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.plate || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.38");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
default:
|
||||
return record.fuel;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.labels.outwith"),
|
||||
dataIndex: "outwith",
|
||||
key: "outwith",
|
||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 ? (
|
||||
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
|
||||
{`${record.cccontracts[0].job.ro_number} - ${OwnerNameDisplayFunction(record.cccontracts[0].job)}`}
|
||||
</Link>
|
||||
) : null
|
||||
},
|
||||
{
|
||||
title: t("contracts.fields.scheduledreturn"),
|
||||
dataIndex: "scheduledreturn",
|
||||
key: "scheduledreturn",
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 && (
|
||||
<DateTimeFormatter>{record.cccontracts[0].scheduledreturn}</DateTimeFormatter>
|
||||
)
|
||||
: courtesycars;
|
||||
}
|
||||
];
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: "courtesycar_inventory",
|
||||
label: t("printcenter.courtesycarcontract.courtesy_car_inventory"),
|
||||
onClick: () =>
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("courtesycar").courtesy_car_inventory.key,
|
||||
variables: {
|
||||
//id: contract.id
|
||||
},
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
),
|
||||
},
|
||||
];
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const menu = {items};
|
||||
const tableData = searchText
|
||||
? courtesycars.filter(
|
||||
(c) =>
|
||||
(c.fleetnumber || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.vin || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(c.plate || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
: courtesycars;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("menus.header.courtesycars")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined/>
|
||||
</Button>
|
||||
<Dropdown trigger="click" menu={menu}>
|
||||
<Button>{t("general.labels.print")}</Button>
|
||||
</Dropdown>
|
||||
<Link to={`/manage/courtesycars/new`}>
|
||||
<Button>{t("courtesycars.actions.new")}</Button>
|
||||
</Link>
|
||||
<Input.Search
|
||||
className="imex-table-header__search"
|
||||
placeholder={t("general.labels.search")}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
value={searchText}
|
||||
enterButton
|
||||
/>
|
||||
</Space>
|
||||
const items = [
|
||||
{
|
||||
key: "courtesycar_inventory",
|
||||
label: t("printcenter.courtesycarcontract.courtesy_car_inventory"),
|
||||
onClick: () =>
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("courtesycar").courtesy_car_inventory.key,
|
||||
variables: {
|
||||
//id: contract.id
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{position: "top"}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={tableData}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const menu = { items };
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("menus.header.courtesycars")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Dropdown trigger="click" menu={menu}>
|
||||
<Button>{t("general.labels.print")}</Button>
|
||||
</Dropdown>
|
||||
<Link to={`/manage/courtesycars/new`}>
|
||||
<Button>{t("courtesycars.actions.new")}</Button>
|
||||
</Link>
|
||||
<Input.Search
|
||||
className="imex-table-header__search"
|
||||
placeholder={t("general.labels.search")}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
value={searchText}
|
||||
enterButton
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={tableData}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
import {useQuery} from "@apollo/client";
|
||||
import {Card, Form, Result} from "antd";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Card, Form, Result } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {QUERY_CSI_RESPONSE_BY_PK} from "../../graphql/csi.queries";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
export default function CsiResponseFormContainer() {
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const {responseid} = searchParams;
|
||||
const {loading, error, data} = useQuery(QUERY_CSI_RESPONSE_BY_PK, {
|
||||
variables: {
|
||||
id: responseid,
|
||||
},
|
||||
skip: !!!responseid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { responseid } = searchParams;
|
||||
const { loading, error, data } = useQuery(QUERY_CSI_RESPONSE_BY_PK, {
|
||||
variables: {
|
||||
id: responseid
|
||||
},
|
||||
skip: !!!responseid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [data, form]);
|
||||
|
||||
if (!!!responseid)
|
||||
return (
|
||||
<Card>
|
||||
<Result title={t("csi.labels.noneselected")}/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (loading) return <LoadingSpinner/>;
|
||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [data, form]);
|
||||
|
||||
if (!!!responseid)
|
||||
return (
|
||||
<Card>
|
||||
<Form form={form} initialValues={data.csi_by_pk.response}>
|
||||
<ConfigFormComponents
|
||||
readOnly
|
||||
componentList={data.csi_by_pk.csiquestion.config}
|
||||
/>
|
||||
{data.csi_by_pk.validuntil ? (
|
||||
<>
|
||||
{t("csi.fields.validuntil")}
|
||||
{": "}
|
||||
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
|
||||
</>
|
||||
) : null}</Form>
|
||||
</Card>
|
||||
<Card>
|
||||
<Result title={t("csi.labels.noneselected")} />
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Form form={form} initialValues={data.csi_by_pk.response}>
|
||||
<ConfigFormComponents readOnly componentList={data.csi_by_pk.csiquestion.config} />
|
||||
{data.csi_by_pk.validuntil ? (
|
||||
<>
|
||||
{t("csi.fields.validuntil")}
|
||||
{": "}
|
||||
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,131 +1,117 @@
|
||||
import {SyncOutlined} from "@ant-design/icons";
|
||||
import {Button, Card, Table} from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link, useLocation, useNavigate} from "react-router-dom";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import OwnerNameDisplay, {OwnerNameDisplayFunction,} from "../owner-name-display/owner-name-display.component";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CsiResponseListPaginated({
|
||||
refetch,
|
||||
loading,
|
||||
responses,
|
||||
total,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const {responseid} = search;
|
||||
const history = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {text: ""},
|
||||
page: "",
|
||||
});
|
||||
export default function CsiResponseListPaginated({ refetch, loading, responses, total }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { responseid } = search;
|
||||
const history = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
page: ""
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner_name",
|
||||
key: "owner_name",
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.job.ownerid}>
|
||||
<OwnerNameDisplay ownerObject={record.job}/>
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record.job}/>
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>{record.job.ro_number || t("general.labels.na")}</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner_name",
|
||||
key: "owner_name",
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.job.ownerid}>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("csi.fields.completedon"),
|
||||
dataIndex: "completedon",
|
||||
key: "completedon",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.completedon, b.completedon),
|
||||
sortOrder: state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.completedon ? (
|
||||
<DateFormatter>{record.completedon}</DateFormatter>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("csi.fields.completedon"),
|
||||
dataIndex: "completedon",
|
||||
key: "completedon",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.completedon, b.completedon),
|
||||
sortOrder: state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.completedon ? <DateFormatter>{record.completedon}</DateFormatter> : null;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter,
|
||||
page: pagination.current,
|
||||
});
|
||||
};
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter,
|
||||
page: pagination.current
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.responseid = record.id;
|
||||
history({search: queryString.stringify(search)});
|
||||
}
|
||||
} else {
|
||||
delete search.responseid;
|
||||
history({search: queryString.stringify(search)});
|
||||
}
|
||||
};
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.responseid = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.responseid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(state.page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={responses}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [responseid],
|
||||
type: "radio",
|
||||
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(state.page || 1),
|
||||
total: total
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={responses}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [responseid],
|
||||
type: "radio"
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,168 +1,183 @@
|
||||
import {Card, Table, Tag} from "antd";
|
||||
import { Card, Table, Tag } from "antd";
|
||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import dayjs from '../../../utils/day';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import dayjs from "../../../utils/day";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import axios from "axios";
|
||||
|
||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, 'day').toLocaleString();
|
||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
||||
|
||||
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
|
||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop)
|
||||
const {t} = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lifecycleData, setLifecycleData] = useState(null);
|
||||
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lifecycleData, setLifecycleData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function getLifecycleData() {
|
||||
if (data && data.job_lifecycle) {
|
||||
setLoading(true);
|
||||
const response = await axios.post("/job/lifecycle", {
|
||||
jobids: data.job_lifecycle.map(x => x.id),
|
||||
statuses: bodyshop.md_ro_statuses
|
||||
});
|
||||
setLifecycleData(response.data.durations);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
async function getLifecycleData() {
|
||||
if (data && data.job_lifecycle) {
|
||||
setLoading(true);
|
||||
const response = await axios.post("/job/lifecycle", {
|
||||
jobids: data.job_lifecycle.map((x) => x.id),
|
||||
statuses: bodyshop.md_ro_statuses
|
||||
});
|
||||
setLifecycleData(response.data.durations);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
getLifecycleData().catch(e => {
|
||||
console.error(`Error in getLifecycleData: ${e}`);
|
||||
})
|
||||
}, [data, bodyshop]);
|
||||
getLifecycleData().catch((e) => {
|
||||
console.error(`Error in getLifecycleData: ${e}`);
|
||||
});
|
||||
}, [data, bodyshop]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('job_lifecycle.columns.status'),
|
||||
dataIndex: 'status',
|
||||
bgColor: 'red',
|
||||
key: 'status',
|
||||
render: (text, record) => {
|
||||
return <Tag color={record.color}>{record.status}</Tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('job_lifecycle.columns.human_readable'),
|
||||
dataIndex: 'humanReadable',
|
||||
key: 'humanReadable',
|
||||
},
|
||||
{
|
||||
title: t('job_lifecycle.columns.status_count'),
|
||||
key: 'statusCount',
|
||||
render: (text, record) => {
|
||||
return lifecycleData.statusCounts[record.status];
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('job_lifecycle.columns.percentage'),
|
||||
dataIndex: 'percentage',
|
||||
key: 'percentage',
|
||||
render: (text, record) => {
|
||||
return record.percentage.toFixed(2) + '%';
|
||||
}
|
||||
},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
title: t("job_lifecycle.columns.status"),
|
||||
dataIndex: "status",
|
||||
bgColor: "red",
|
||||
key: "status",
|
||||
render: (text, record) => {
|
||||
return <Tag color={record.color}>{record.status}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("job_lifecycle.columns.human_readable"),
|
||||
dataIndex: "humanReadable",
|
||||
key: "humanReadable"
|
||||
},
|
||||
{
|
||||
title: t("job_lifecycle.columns.status_count"),
|
||||
key: "statusCount",
|
||||
render: (text, record) => {
|
||||
return lifecycleData.statusCounts[record.status];
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("job_lifecycle.columns.percentage"),
|
||||
dataIndex: "percentage",
|
||||
key: "percentage",
|
||||
render: (text, record) => {
|
||||
return record.percentage.toFixed(2) + "%";
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (!data) return null;
|
||||
if (!data) return null;
|
||||
|
||||
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
||||
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
|
||||
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
|
||||
|
||||
return (
|
||||
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<div style={{overflow: 'scroll', height: "100%"}}>
|
||||
<div id="bar-container" style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100px',
|
||||
textAlign: 'center',
|
||||
borderRadius: '5px',
|
||||
borderWidth: '5px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: '#f0f2f5',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}}>
|
||||
{lifecycleData.summations.map((key, index, array) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === array.length - 1;
|
||||
return (
|
||||
<div key={key.status} style={{
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
return (
|
||||
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<div style={{ overflow: "scroll", height: "100%" }}>
|
||||
<div
|
||||
id="bar-container"
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100px",
|
||||
textAlign: "center",
|
||||
borderRadius: "5px",
|
||||
borderWidth: "5px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "#f0f2f5",
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
{lifecycleData.summations.map((key, index, array) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === array.length - 1;
|
||||
return (
|
||||
<div
|
||||
key={key.status}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
|
||||
borderTop: '1px solid #f0f2f5',
|
||||
borderBottom: '1px solid #f0f2f5',
|
||||
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
|
||||
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
|
||||
borderTop: "1px solid #f0f2f5",
|
||||
borderBottom: "1px solid #f0f2f5",
|
||||
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
|
||||
borderRight: isLast ? "1px solid #f0f2f5" : undefined,
|
||||
|
||||
backgroundColor: key.color,
|
||||
width: `${key.percentage}%`
|
||||
}}
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
>
|
||||
|
||||
{key.percentage > 15 ?
|
||||
<>
|
||||
<div>{key.roundedPercentage}</div>
|
||||
<div style={{
|
||||
backgroundColor: '#f0f2f5',
|
||||
borderRadius: '5px',
|
||||
paddingRight: '2px',
|
||||
paddingLeft: '2px',
|
||||
fontSize: '0.8rem',
|
||||
}}>
|
||||
{key.status}
|
||||
</div>
|
||||
</>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
|
||||
style={{marginTop: '10px'}}>
|
||||
<div>
|
||||
{lifecycleData.summations.map((key) => (
|
||||
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
style={{
|
||||
backgroundColor: '#f0f2f5',
|
||||
color: '#000',
|
||||
padding: '4px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
|
||||
</div>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
|
||||
<Table size="small" pagination={false} columns={columns}
|
||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
|
||||
</Card>
|
||||
backgroundColor: key.color,
|
||||
width: `${key.percentage}%`
|
||||
}}
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
>
|
||||
{key.percentage > 15 ? (
|
||||
<>
|
||||
<div>{key.roundedPercentage}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#f0f2f5",
|
||||
borderRadius: "5px",
|
||||
paddingRight: "2px",
|
||||
paddingLeft: "2px",
|
||||
fontSize: "0.8rem"
|
||||
}}
|
||||
>
|
||||
{key.status}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</LoadingSkeleton>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Card
|
||||
extra={extra}
|
||||
type="inner"
|
||||
title={t("job_lifecycle.content.legend_title")}
|
||||
style={{ marginTop: "10px" }}
|
||||
>
|
||||
<div>
|
||||
{lifecycleData.summations.map((key) => (
|
||||
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
style={{
|
||||
backgroundColor: "#f0f2f5",
|
||||
color: "#000",
|
||||
padding: "4px",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
|
||||
</div>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ marginTop: "5px" }} type="inner" title={t("job_lifecycle.titles.top_durations")}>
|
||||
<Table
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</LoadingSkeleton>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const JobLifecycleDashboardGQL = `
|
||||
job_lifecycle: jobs(where: {
|
||||
actual_in: {
|
||||
_gte: "${dayjs().subtract(45, 'day').toISOString()}"
|
||||
_gte: "${dayjs().subtract(45, "day").toISOString()}"
|
||||
}
|
||||
}) {
|
||||
id
|
||||
|
||||
@@ -1,159 +1,124 @@
|
||||
import {Card} from "antd";
|
||||
import { Card } from "antd";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis,} from "recharts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyEmployeeEfficiency({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
export default function DashboardMonthlyEmployeeEfficiency({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
|
||||
dayjs(item.date).format("YYYY-MM-DD")
|
||||
);
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) => dayjs(item.date).format("YYYY-MM-DD"));
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.productive + dayVal.productivehrs,
|
||||
};
|
||||
},
|
||||
{actual: 0, productive: 0}
|
||||
);
|
||||
} else {
|
||||
dailyHrs = {actual: 0, productive: 0};
|
||||
}
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.productive + dayVal.productivehrs
|
||||
};
|
||||
},
|
||||
{ actual: 0, productive: 0 }
|
||||
);
|
||||
} else {
|
||||
dailyHrs = { actual: 0, productive: 0 };
|
||||
}
|
||||
|
||||
const dailyEfficiency =
|
||||
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
|
||||
const dailyEfficiency = ((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
|
||||
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
// ...dailyHrs,
|
||||
actual: dailyHrs.actual.toFixed(1),
|
||||
productive: dailyHrs.productive.toFixed(1),
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accActual + dailyHrs.actual
|
||||
: dailyHrs.actual,
|
||||
const theValue = {
|
||||
date: dayjs(val).format("DD"),
|
||||
// ...dailyHrs,
|
||||
actual: dailyHrs.actual.toFixed(1),
|
||||
productive: dailyHrs.productive.toFixed(1),
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual: acc.length > 0 ? acc[acc.length - 1].accActual + dailyHrs.actual : dailyHrs.actual,
|
||||
|
||||
accProductive:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accProductive + dailyHrs.productive
|
||||
: dailyHrs.productive,
|
||||
accEfficiency: 0,
|
||||
};
|
||||
accProductive: acc.length > 0 ? acc[acc.length - 1].accProductive + dailyHrs.productive : dailyHrs.productive,
|
||||
accEfficiency: 0
|
||||
};
|
||||
|
||||
theValue.accEfficiency =
|
||||
((theValue.accProductive - theValue.accActual) /
|
||||
(theValue.accActual || 0) +
|
||||
1) *
|
||||
100;
|
||||
theValue.accEfficiency = ((theValue.accProductive - theValue.accActual) / (theValue.accActual || 0) + 1) * 100;
|
||||
|
||||
if (isNaN(theValue.accEfficiency)) {
|
||||
theValue.accEfficiency = 0;
|
||||
} else {
|
||||
theValue.accEfficiency = theValue.accEfficiency.toFixed(1);
|
||||
}
|
||||
if (isNaN(theValue.accEfficiency)) {
|
||||
theValue.accEfficiency = 0;
|
||||
} else {
|
||||
theValue.accEfficiency = theValue.accEfficiency.toFixed(1);
|
||||
}
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyemployeeefficiency")}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{height: "100%"}}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{top: 20, right: 20, bottom: 20, left: 20}}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5"/>
|
||||
<XAxis dataKey="date"/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
stroke="#8884d8"
|
||||
unit=" hrs"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="#82ca9d"
|
||||
unit="%"
|
||||
/>
|
||||
<Tooltip/>
|
||||
<Legend/>
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
format={"0.0"}
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
format={"0.0"}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlyemployeeefficiency")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={chartData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis yAxisId="left" orientation="left" stroke="#8884d8" unit=" hrs" />
|
||||
<YAxis yAxisId="right" orientation="right" stroke="#82ca9d" unit="%" />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
format={"0.0"}
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
format={"0.0"}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyEmployeeEfficiencyGql = `
|
||||
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${dayjs()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}} ]}) {
|
||||
.format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs().endOf("month").format("YYYY-MM-DD")}"}} ]}) {
|
||||
actualhrs
|
||||
productivehrs
|
||||
employeeid
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user