- Merge client update into test-beta

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-18 19:20:08 -05:00
696 changed files with 92291 additions and 107075 deletions

View File

@@ -2,14 +2,15 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import moment from "moment";
import dayjs from "../utils/day";
import 'dayjs/locale/en';
import React from "react";
import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
moment.locale("en-US");
dayjs.locale("en");
export const factory = SplitSdk({
core: {

View File

@@ -1,11 +1,11 @@
import { useClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import {useSplitClient} from "@splitsoftware/splitio-react";
import {Button, Result} from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import React, {lazy, Suspense, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Route, Routes} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
@@ -13,149 +13,136 @@ import LoadingSpinner from "../components/loading-spinner/loading-spinner.compon
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import {setOnline} from "../redux/application/application.actions";
import {selectOnline} from "../redux/application/application.selectors";
import {checkUserSession} from "../redux/user/user.actions";
import {selectBodyshop, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
import("../pages/reset-password/reset-password.component")
);
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container")
import("../pages/mobile-payment/mobile-payment.container")
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
online: selectOnline,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useClient();
export function App({bodyshop, checkUserSession, currentUser, online, setOnline}) {
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
const { t } = useTranslation();
window.addEventListener("offline", function (e) {
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
LogRocket.init("rome-online/rome-online");
if (client.getTreatment("LogRocket_Tracking") === "on") {
LogRocket.init("rome-online/rome-online");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
/>
);
return (
<Switch>
<Suspense fallback={<LoadingSpinner />}>
<ErrorBoundary>
<Route exact path="/" component={LandingPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/signin" component={SignInPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/resetpassword" component={ResetPassword} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense>
</Switch>
);
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
}
const onlineListener = (e) => {
setOnline(true);
}
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.romeonline.io'
) {
console.log("LR Start");
LogRocket.init("rome-online/rome-online");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
}
handleBeta();
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
// Any route that is not assigned and matched will default to the Landing Page component
return (
<Suspense fallback={<LoadingSpinner message="Rome Online"/>}>
<Routes>
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
<Route path="/manage/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<ManagePage/>}/>
</Route>
<Route path="/tech/*" element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
<Route path="*" element={<TechPageContainer/>}/>
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route path="*" element={<DocumentEditorContainer/>}/>
</Route>
</Routes>
</Suspense>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,6 +1,14 @@
//Global Styles.
@import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.ant-menu-dark .ant-menu-item:hover {
background-color: #1890ff !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;

View File

@@ -0,0 +1,17 @@
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();
useEffect(() => {
if (!isAuthorized) {
navigate(`/signin?redirect=${location.pathname}`);
}
}, [isAuthorized, navigate, location]);
return <Outlet/>;
}
export default PrivateRoute;

View File

@@ -1,31 +1,31 @@
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);

View File

@@ -1,233 +1,233 @@
import { Input, Table, Checkbox, Card, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {Card, Checkbox, Input, Space, Table} from "antd";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import {alphaSort, dateSort} from "../../utils/sorters";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import {DateFormatter} from "../../utils/DateFormatter";
import queryString from "query-string";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {logImEXEvent} from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
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 ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config";
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
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: "",
});
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 }),
}}
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",
sorter: (a, b) => a.clm_total - b.clm_total,
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>
}
>
{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",
sorter: (a, b) => a.clm_total - b.clm_total,
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>
);
<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>
);
}

View File

@@ -1,14 +1,14 @@
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 { alphaSort, dateSort } from "../../utils/sorters";
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
@@ -18,215 +18,215 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
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
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: "",
});
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(a.job.ownr_ln, b.job.ownr_ln),
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(a.job.ownr_ln, b.job.ownr_ln),
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",
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",
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",
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",
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",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs}/>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
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>
);
}

View File

@@ -1,247 +1,247 @@
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 } from "../../utils/sorters";
import {alphaSort, dateSort} 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 {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import {DateFormatter} from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.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
mapStateToProps,
mapDispatchToProps
)(AccountingReceivablesTableComponent);
export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
refetch,
}) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
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) => a.status - b.status,
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(a.ownr_ln, b.ownr_ln),
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) => a.status - b.status,
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(a.ownr_ln, b.ownr_ln),
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,
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",
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
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}
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",
}}
/>
)}
{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>
);
</Card>
);
}

View File

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

View File

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

View File

@@ -1,72 +1,72 @@
import { Select, Button, Popover, InputNumber } 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} visible={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);

View File

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

View File

@@ -1,47 +1,47 @@
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);
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}
/>
);
}

View File

@@ -1,68 +1,68 @@
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} visible={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>
);
});

View File

@@ -1,47 +1,47 @@
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,
});
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();
jobLines,
refetch,
}) {
const visibilityState = useState(false);
const {t} = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null,
});
};
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
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}
/>
);
}

View File

@@ -1,20 +1,20 @@
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.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>
);
}

View File

@@ -1,32 +1,32 @@
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}
/>
);
}

View File

@@ -1,85 +1,85 @@
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";
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}
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});
};
return (
<Table
{...formItemLayout}
loading={loading}
pagination={{position: "top", defaultPageSize: pageLimit}}
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
),
},
{
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 });
};
return (
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
);
}

View File

@@ -1,40 +1,40 @@
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>
);
}

View File

@@ -1,64 +1,64 @@
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 {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}
/>
);
}

View File

@@ -1,29 +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";
export default function AuditTrailValuesComponent({ oldV, newV }) {
if (!oldV && !newV) return <div></div>;
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>
);
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>
<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>
);
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>
);
}

View File

@@ -1,22 +1,23 @@
import { Tag, Popover } from "antd";
import {Popover, Tag} from "antd";
import React from "react";
import Barcode from "react-barcode";
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>
);
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>
);
}

View File

@@ -1,135 +1,136 @@
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();
form,
returnLoading,
returnData,
}) {
const {t} = useTranslation();
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;
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
}
}, [returnData, form]);
if (returnLoading) return <Skeleton />;
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");
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 (!isReturn) {
return null;
}
<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>
if (returnLoading) return <Skeleton/>;
<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>
</>
);
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>
);
}}
</Form.List>
);
}}
</Form.Item>
);
</Form.Item>
);
}

View File

@@ -1,19 +1,19 @@
.bill-cm-returns-table {
table-layout: fixed;
width: 100%;
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -1,80 +1,80 @@
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";
export default function BillDeleteButton({ bill, callback }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
export default function BillDeleteButton({bill, callback}) {
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)
);
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)
);
},
},
});
},
search_bills(existingBills, { readField }) {
return existingBills.filter(
(billref) => bill.id !== readField("id", billref)
);
},
},
});
},
});
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
if (!!!result.errors) {
notification["success"]({message: t("bills.successes.deleted")});
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>
);
}

View File

@@ -1,21 +1,17 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import {useMutation, useQuery} from "@apollo/client";
import {Button, 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";
@@ -26,227 +22,226 @@ 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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
});
export default connect(
mapStateToProps,
mapDispatchToProps
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 [visible, setVisible] = 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 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
)
setVisible(true);
else {
form.submit();
}
};
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;
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid},
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
//Find bill lines that were deleted.
const deletedJobLines = [];
// ... rest of the code remains the same
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
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();
}
};
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
const {billlines, upload, ...bill} = values;
const updates = [];
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
update_bill({
variables: {billId: search.billid, bill: bill},
})
);
} 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);
billlines.forEach((l) => {
delete l.selected;
});
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
//Find bill lines that were deleted.
const deletedJobLines = [];
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({variables: {id: d.id}}));
});
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
billlines.forEach((billline) => {
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
return (
<>
{loading && <LoadingSkeleton />}
{data && (
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),
});
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 (
<>
<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
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(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} />
{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}/>
{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}
/>
{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>
</>
)}
</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 ? moment(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,
}
: {};
};

View File

@@ -1,185 +1,185 @@
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 { useHistory, useLocation } 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 }) =>
dispatch(insertAuditTrail({ jobid, operation })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
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.push({ search: queryString.stringify(search) });
setVisible(false);
};
useEffect(() => {
if (visible === false) form.resetFields();
}, [visible, form]);
history({search: queryString.stringify(search)});
setOpen(false);
};
useEffect(() => {
if (open === false) form.resetFields();
}, [open, form]);
return (
<>
<Modal
visible={visible}
onCancel={() => setVisible(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 || disabled}
onClick={() => {
setVisible(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 || disabled}
onClick={() => {
setOpen(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
}

View File

@@ -1,40 +1,40 @@
import { Drawer, Grid } from "antd";
import {Drawer, Grid} from "antd";
import queryString from "query-string";
import React from "react";
import { useHistory, useLocation } 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 = useHistory();
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.push({ search: queryString.stringify(search) });
}}
destroyOnClose
visible={search.billid}
>
<BillDetailEditComponent />
</Drawer>
);
return (
<Drawer
width={drawerPercentage}
onClose={() => {
delete search.billid;
history({search: queryString.stringify(search)});
}}
destroyOnClose
open={search.billid}
>
<BillDetailEditComponent/>
</Drawer>
);
}

View File

@@ -1,470 +1,466 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Modal, Space, notification } 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 }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })),
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({jobid, billid, operation}) =>
dispatch(insertAuditTrail({jobid, billid, operation})),
});
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
);
const { Enhanced_Payroll } = useTreatments(
["Enhanced_Payroll"],
{},
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,
...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"],
});
await Promise.all(
payrollAdjustmentsToInsert.map((li) => {
return updateJobLines({
variables: {
lineId: li.id,
line: {
convertedtolbr: li.convertedtolbr,
convertedtolbr_data: li.convertedtolbr_data,
},
},
});
})
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 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 {treatments: {Enhanced_Payroll}} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid,
});
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
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]);
adjKeys.forEach((key) => {
newAdjustments[key] =
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
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,
...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"],
});
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),
}),
});
});
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)},
});
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();
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
});
});
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) },
});
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 (enterAgain) {
// form.resetFields();
form.setFieldsValue({
...formValues,
billlines: [],
});
})
);
}
form.resetFields();
} else {
toggleModalVisible();
}
setEnterAgain(false);
};
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
const handleCancel = () => {
const r = window.confirm(t("general.labels.cancel"));
if (r === true) {
toggleModalVisible();
}
};
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,
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);
}}
>
{t("general.actions.saveandnew")}
</Button>
)}
</Space>
}
);
});
}
}
///////////////////////////
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();
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
});
if (enterAgain) {
// form.resetFields();
form.setFieldsValue({
...formValues,
billlines: [],
});
form.resetFields();
} else {
toggleModalVisible();
}
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.visible) {
form.setFieldsValue(formValues);
} else {
form.resetFields();
}
}, [billEnterModal.visible, form, formValues]);
return (
<Modal
title={t("bills.labels.new")}
width={"98%"}
visible={billEnterModal.visible}
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);
}}
destroyOnClose
>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
onFinishFailed={() => {
setEnterAgain(false);
}}
>
{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>
);
<BillFormContainer
form={form}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(BillEnterModalContainer);

View File

@@ -1,135 +1,136 @@
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 = [
{
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: [
lineData,
discount,
form,
responsibilityCenters,
disabled,
}) {
const [search, setSearch] = useState("");
const {t} = useTranslation();
const columns = [
{
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
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("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"],
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.PAL"),
value: ["PAL"],
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.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}
/>
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>
),
},
];
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>
);
);
}

View File

@@ -1,288 +1,284 @@
import React from "react";
import {
PlusCircleFilled,
MinusCircleFilled,
WarningOutlined,
} from "@ant-design/icons";
import { Form, Button, InputNumber, Input, Select, Switch, Space } 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
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({
value,
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount,
}) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
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>
);
const { t } = useTranslation();
if (!value)
return (
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
<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);
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>
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>
);
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>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,83 @@
import { useLazyQuery, useQuery } from "@apollo/client";
import { useTreatments } 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 { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
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

View File

@@ -1,47 +1,47 @@
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,
};
};

View File

@@ -1,173 +1,173 @@
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();
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,
});
}
}, [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;
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]);
if (inventoryLoading) return <Skeleton />;
return (
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
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 (!is_inhouse || billEdit) {
return null;
}
<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>
if (inventoryLoading) return <Skeleton/>;
<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>
</>
);
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>
);
}}
</Form.List>
);
}}
</Form.Item>
);
</Form.Item>
);
}

View File

@@ -1,19 +1,19 @@
.bill-inventory-table {
table-layout: fixed;
width: 100%;
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -1,83 +1,87 @@
import { Select } from "antd";
import React, { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import {Select} from "antd";
import React, {forwardRef} from "react";
import {useTranslation} from "react-i18next";
//To be used as a form element only.
const { Option } = Select;
const {Option} = Select;
const BillLineSearchSelect = (
{ options, disabled, allowRemoved, ...restProps },
ref
{options, disabled, allowRemoved, ...restProps},
ref
) => {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<Select
disabled={disabled}
ref={ref}
showSearch
dropdownMatchSelectWidth={false}
// 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" } : {}),
}}
>
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>
{item.act_price === 0 && item.mod_lb_hrs > 0 && (
<span style={{ float: "right", paddingleft: "1rem" }}>
{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" }}>
<span style={{float: "right", paddingleft: "1rem"}}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
? `$${item.act_price && item.act_price.toFixed(2)}`
: ``}
</span>
</Option>
))
: null}
</Select>
);
</Option>
))
: null}
</Select>
);
};
export default forwardRef(BillLineSearchSelect);

View File

@@ -1,101 +1,97 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
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
mapStateToProps,
mapDispatchToProps
)(BillMarkExportedButton);
export function BillMarkExportedButton({
currentUser,
bodyshop,
authLevel,
bill,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
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 },
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",
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
},
],
},
});
if (hasAccess)
return (
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
);
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 <></>;
return <></>;
}

View File

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

View File

@@ -1,82 +1,79 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
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
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 },
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",
});
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.
};
if (hasAccess)
return (
<Button
loading={loading}
disabled={!bill.exported}
onClick={handleUpdate}
>
{t("bills.labels.markforreexport")}
</Button>
);
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 <></>;
return <></>;
}

View File

@@ -1,155 +1,151 @@
import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import moment from "moment";
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 {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 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
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const {billid} = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
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,
},
},
],
};
const cm = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: moment().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),
}),
});
}
setLoading(false);
};
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: moment().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>
);
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>
);
}

View File

@@ -1,235 +1,236 @@
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) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled />
</Button>
)}
<BillDeleteButton bill={record} />
<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={
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery;
const recordActions = (record, showView = false) => (
<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}
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled/>
</Button>
)}
<BillDeleteButton bill={record}/>
<BillDetailEditReturnComponent
data={{bills_by_pk: {...record, jobid: job.id}}}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
/>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{
name: Templates.inhouse_invoice.key,
variables: {id: record.id},
}}
messageObject={{subject: Templates.inhouse_invoice.subject}}
/>
)}
</Space>
}
>
<Table
loading={billsQuery.loading}
scroll={{
x: true, // y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>
);
);
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"
}}
columns={columns}
rowKey="id"
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(BillsListTableComponent);

View File

@@ -1,121 +1,121 @@
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 { useHistory, useLocation } from "react-router-dom";
import { Table, Input } 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 = useHistory();
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,
},
{
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);
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,
},
selectedRowKeys: [search.vendorid],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/>
);
{
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
};
}}
/>
);
}

View File

@@ -1,54 +1,64 @@
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Row, Col } 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 { useTreatments } 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 { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
export function BreadCrumbs({breadcrumbs, bodyshop}) {
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />{" "}
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
""}
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col>
</Row>
);
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);

View File

@@ -1,99 +1,99 @@
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();
caBcEtfTableModal,
toggleModalVisible,
}) {
const {t} = useTranslation();
const { visible } = 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"
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();
}
}, [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>
);
setLoading(false);
};
useEffect(() => {
if (visible) {
form.resetFields();
}
}, [visible, form]);
return (
<Modal
visible={visible}
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
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -1,42 +1,42 @@
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>
);
}

View File

@@ -1,49 +1,50 @@
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);
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";
const { t } = useTranslation();
export default function CABCpvrtCalculator({disabled, form}) {
const [visibility, setVisibility] = useState(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 {t} = useTranslation();
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 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);
};
return (
<Popover
destroyTooltipOnHide
content={popContent}
visible={visibility}
disabled={disabled}
>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled />
</Button>
</Popover>
);
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>
);
}

View File

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

View File

@@ -1,57 +1,57 @@
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 { visible } = cardPaymentModal;
const { t } = useTranslation();
cardPaymentModal,
toggleModalVisible,
bodyshop,
}) {
const {open} = cardPaymentModal;
const {t} = useTranslation();
const handleCancel = () => {
toggleModalVisible();
};
const handleCancel = () => {
toggleModalVisible();
};
const handleOK = () => {
toggleModalVisible();
};
const handleOK = () => {
toggleModalVisible();
};
return (
<Modal
open={visible}
onOk={handleOK}
onCancel={handleCancel}
footer={[
<Button key="back" onClick={handleCancel}>
{t("job_payments.buttons.goback")}
</Button>,
]}
width="80%"
destroyOnClose
>
<CardPaymentModalComponent />
</Modal>
);
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
mapStateToProps,
mapDispatchToProps
)(CardPaymentModalContainer);

View File

@@ -1,106 +1,98 @@
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 { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
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();
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
async function SubscribeToTopic() {
try {
const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: process.env.REACT_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({
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>
),
});
}
}
async function SubscribeToTopic() {
try {
const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: process.env.REACT_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({
type: "warning",
message: t("general.errors.fcm"),
btn: (
<Space>
<Button
onClick={async () => {
await requestForToken();
SubscribeToTopic();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop]);
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,
});
}
let stopMessageListenr, channel;
try {
stopMessageListenr = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage);
} catch (error) {
console.log("Unable to set event listeners.");
}
return () => {
stopMessageListenr && stopMessageListenr();
channel && channel.removeEventListener("message", handleMessage);
};
}, [client]);
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div>
);
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent/> : null}
</div>
);
}
export default connect(mapStateToProps, null)(ChatAffixContainer);
export default ChatAffixContainer;

View File

@@ -1,29 +1,29 @@
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>
);
}

View File

@@ -1,120 +1,122 @@
import { Badge, List, 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 OwnerNameDisplay 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 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 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>
);
};
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
>
<div
style={{
display: "inline-block",
}}
>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div style={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
<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>
);
};
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
mapStateToProps,
mapDispatchToProps
)(ChatConversationListComponent);

View File

@@ -1,27 +1,16 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-container {
flex: 1;
overflow: hidden;
height: 100%;
border: 1px solid gainsboro;
}
.chat-list-item {
display: flex;
flex-direction: row;
.ant-card-head {
border: none;
}
&:hover {
cursor: pointer;
color: #ff7a00;
}
.chat-name {
flex: 1;
display: inline;
}
.ro-number-tag {
align-self: baseline;
}
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
}

View File

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

View File

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

View File

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

View File

@@ -1,89 +1,87 @@
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 [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 [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 unreadCount =
data &&
data.messages &&
data.messages.reduce((acc, val) => {
return !val.read && !val.isoutbound ? acc + 1 : acc;
}, 0);
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);
}
};
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}
/>
);
}

View File

@@ -1,67 +1,68 @@
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);
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";
const { t } = useTranslation();
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
export default function ChatLabel({conversation}) {
const [loading, setLoading] = useState(false);
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(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);
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>
);
}
};
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>
);
}
}

View File

@@ -1,99 +1,100 @@
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 [visible, setVisible] = useState(false);
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:
!visible ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
});
skip:
!open ||
!conversation.job_conversations ||
conversation.job_conversations.length === 0,
});
const handleVisibleChange = (visible) => {
setVisible(visible);
};
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 && visible && (
<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"
visible={visible}
onVisibleChange={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>
);
}

View File

@@ -1,118 +1,113 @@
import Icon from "@ant-design/icons";
import { Tooltip } from "antd";
import {Tooltip} from "antd";
import i18n from "i18next";
import moment from "moment";
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 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 "./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]);
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>
);
};
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: moment(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a"
),
})}
</div>
)}
</div>
)}
</CellMeasurer>
<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>
);
};
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>
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>
</div>
))}
<div>{message.text}</div>
</div>
</Tooltip>
);
</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;
}
};

View File

@@ -44,6 +44,7 @@
.yours {
align-items: flex-start;
}
.msgmargin {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
@@ -66,6 +67,7 @@
background: #eee;
border-bottom-right-radius: 15px;
}
.yours .message.last:after {
content: "";
position: absolute;

View File

@@ -1,57 +1,55 @@
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
mapStateToProps,
mapDispatchToProps
)(ChatNewConversation);

View File

@@ -1,52 +1,54 @@
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 <></>;
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);

View File

@@ -1,24 +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";
@@ -26,118 +15,119 @@ 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);
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 [getConversations, { loading, data, refetch, fetchMore }] =
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
...(pollInterval > 0 ? {pollInterval} : {}),
});
const fcmToken = sessionStorage.getItem("fcmtoken");
const [getConversations, {loading, data, refetch, fetchMore}] =
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? {pollInterval} : {}),
});
useEffect(() => {
if (fcmToken) {
setpollInterval(0);
} else {
setpollInterval(60000);
}
}, [fcmToken]);
const fcmToken = sessionStorage.getItem("fcmtoken");
useEffect(() => {
if (chatVisible)
getConversations({
variables: {
offset: 0,
},
});
}, [chatVisible, getConversations]);
useEffect(() => {
if (fcmToken) {
setpollInterval(0);
} else {
setpollInterval(60000);
}
}, [fcmToken]);
const loadMoreConversations = useCallback(() => {
if (data)
fetchMore({
variables: {
offset: data.conversations.length,
},
});
}, [data, fetchMore]);
useEffect(() => {
if (chatVisible)
getConversations({
variables: {
offset: 0,
},
});
}, [chatVisible, getConversations]);
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
const loadMoreConversations = useCallback(() => {
if (data)
fetchMore({
variables: {
offset: data.conversations.length,
},
});
}, [data, fetchMore]);
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" }}
/>
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
<Row gutter={[8, 8]} className="chat-popup-content">
<Col span={8}>
{loading ? (
<LoadingSpinner />
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>
) : (
<ChatConversationListComponent
conversationList={data ? data.conversations : []}
loadMoreConversations={loadMoreConversations}
/>
<div
onClick={() => toggleChatVisible()}
style={{cursor: "pointer"}}
>
<MessageOutlined className="chat-popup-info-icon"/>
<strong>{t("messaging.labels.messaging")}</strong>
</div>
)}
</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>
);
</Card>
</Badge>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);

View File

@@ -13,6 +13,7 @@
height: 100%;
}
}
.chat-popup-info-icon {
margin-right: 5px;
}

View File

@@ -1,40 +1,38 @@
import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown, Menu } 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 }) {
const menu = (
<Menu>
{bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
{i.label}
</Menu.Item>
))}
</Menu>
);
export function ChatPresetsComponent({bodyshop, setMessage, className}) {
return (
<div className={className}>
<Dropdown trigger={["click"]} overlay={menu}>
<PlusCircleOutlined />
</Dropdown>
</div>
);
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>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ChatPresetsComponent);

View File

@@ -1,59 +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);
return (
<Space wrap>
<PrinterOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
const generateDocument = (type) => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
name: TemplateList("messaging").conversation_list.key,
variables: {id: conversation.id},
},
{
subject: TemplateList("messaging").conversation_list.subject,
subject: TemplateList("messaging").conversation_list.subject,
},
"p",
type,
conversation.id
);
setLoading(false);
}}
/>
<MailOutlined
onClick={() => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
"e",
conversation.id
);
setLoading(false);
}}
/>
{loading && <Spin />}
</Space>
);
).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>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);

View File

@@ -1,116 +1,111 @@
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]);
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
mapStateToProps,
mapDispatchToProps
)(ChatSendMessageComponent);

View File

@@ -1,45 +1,45 @@
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,
setVisible,
}) {
const { t } = useTranslation();
roOptions,
loading,
handleSearch,
handleInsertTag,
setOpen,
}) {
const {t} = useTranslation();
return (
<Space flex>
<div style={{ width: "15rem" }}>
<Select
showSearch
autoFocus
dropdownMatchSelectWidth
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={() => setVisible(false)} />
)}
</Space>
);
{loading ? (
<LoadingOutlined/>
) : (
<CloseCircleOutlined onClick={() => setOpen(false)}/>
)}
</Space>
);
}

View File

@@ -1,66 +1,66 @@
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 [visible, setVisible] = 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 } });
setVisible(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>
{visible ? (
<ChatTagRo
loading={loading}
roOptions={roOptions}
handleSearch={handleSearch}
handleInsertTag={handleInsertTag}
setVisible={setVisible}
/>
) : (
<Tag onClick={() => setVisible(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>
);
}

View File

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

View File

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

View File

@@ -3,12 +3,13 @@ import Rate from "./rate/rate.component";
import Slider from "./slider/slider.component";
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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,27 @@
import React from "react";
import { Result, Button } from "antd";
import { useTranslation } from "react-i18next";
import {Button, Result} from "antd";
import {useTranslation} from "react-i18next";
export default function ConflictComponent() {
const { t } = useTranslation();
return (
<div>
<Result
status="warning"
title={t("general.labels.instanceconflictitle")}
extra={
<div>
<div>{t("general.labels.instanceconflictext")}</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")}</div>
<Button
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
</div>
}
/>
</div>
);
}

View File

@@ -1,152 +1,152 @@
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: "",
});
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 })}
/>
}
>
<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>
);
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);
},
};
}}
/>
</Card>
);
}

View File

@@ -1,37 +1,37 @@
import { useQuery } from "@apollo/client";
import moment from "moment";
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: moment().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,
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",
});
};
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<ContractCarsComponent
handleSelect={handleSelect}
selectedCarId={selectedCar && selectedCar.id}
loading={loading}
data={data ? data.courtesycars : []}
/>
);
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 : []}
/>
);
}

View File

@@ -1,408 +1,397 @@
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 moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
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";
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 [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [insertJob] = useMutation(INSERT_NEW_JOB);
const history = useHistory();
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 = moment(contract.actualreturn).diff(
moment(contract.start),
"days"
);
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,
});
}
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,
},
},
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);
};
//Calcualte the new job totals.
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>
);
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}`
);
},
});
}
setVisible(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={() => setVisible(false)}>
{t("general.actions.close")}
</Button>
</Space>
</Form>
</div>
);
return (
<div>
<Popover content={popContent} visible={visible}>
<Button
onClick={() => setVisible(true)}
loading={loading}
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
>
{t("contracts.actions.convertoro")}
</Button>
</Popover>
</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
mapStateToProps,
mapDispatchToProps
)(ContractConvertToRo);

View File

@@ -1,32 +1,32 @@
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>
);
}

View File

@@ -1,44 +1,45 @@
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();
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";
const handleClick = () => {
call({ variables: { id: jobId } });
};
export default function ContractCreateJobPrefillComponent({jobId, form}) {
const [call, {loading, error, data}] = useLazyQuery(
GET_JOB_FOR_CC_CONTRACT
);
const {t} = useTranslation();
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,
});
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),
}),
});
}
}, [data, form]);
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>
);
return (
<Button onClick={handleClick} disabled={!jobId} loading={loading}>
{t("contracts.labels.populatefromjob")}
</Button>
);
}

View File

@@ -1,9 +1,9 @@
import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd";
import moment from "moment";
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,339 +11,337 @@ 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>
)}
</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 <= form.getFieldValue("kmstart");
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>
)}
</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 <= form.getFieldValue("kmstart");
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore(
moment(form.getFieldValue("scheduledreturn"))
);
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
dayjs(selectedCar.nextservicedate).isBefore(
dayjs(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
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>
<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>
<div>
<Space wrap>
{selectedJobState && (
<div>
<ContractFormJobPrefill
jobId={selectedJobState && selectedJobState[0]}
form={form}
/>
</div>
)}
{
//<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 = moment(
form.getFieldValue("driver_dlexpiry")
).isBefore(moment(form.getFieldValue("scheduledreturn")));
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>
return <></>;
}}
</Form.Item>
)}
</div>
);
}}
</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>
<ContractFormJobPrefill
jobId={selectedJobState && selectedJobState[0]}
form={form}
/>
</div>
)}
{
//<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")));
<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>
);
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>
);
}

View File

@@ -1,32 +1,33 @@
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>
);
}

View File

@@ -1,201 +1,201 @@
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";
export default function ContractsJobsComponent({
loading,
data,
selectedJob,
handleSelect,
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
search: "",
});
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>
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.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>
{
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 || ""
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 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]);
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);
) : (
t("jobs.errors.novehicle")
);
},
};
}}
/>
</Card>
);
},
{
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 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]);
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);
},
};
}}
/>
</Card>
);
}

View File

@@ -1,39 +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;
const handleSelect = (record) => {
setSelectedJob(record.id);
};
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;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<ContractJobsComponent
handleSelect={handleSelect}
selectedJob={selectedJob}
loading={loading}
data={data ? data.jobs : []}
/>
);
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 : []}
/>
);
}
export default connect(mapStateToProps, null)(ContractJobsContainer);

View File

@@ -1,123 +1,123 @@
import { Button, Input, Modal, Typography } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {Button, Input, Modal, Typography} from "antd";
import dayjs from "../../utils/day";
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 handleInsertForm = () => {
logImEXEvent("contract_license_decode_fill_form");
const values = {
driver_dlnumber: decodedBarcode.dl,
driver_dlexpiry: moment(
`20${decodedBarcode.expiration_date}${moment(
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: moment(decodedBarcode.birthday),
const handleDecode = (e) => {
logImEXEvent("contract_license_decode");
setLoading(true);
const aamvaParse = aamva.parse(e.currentTarget.value);
setDecodedBarcode(aamvaParse);
setLoading(false);
};
form.setFieldsValue(values);
setModalVisible(false);
setDecodedBarcode(null);
};
const handleClick = () => {
setModalVisible(true);
};
const handleCancel = () => {
setModalVisible(false);
};
const handleInsertForm = () => {
logImEXEvent("contract_license_decode_fill_form");
return (
<div>
<Modal
visible={modalVisible}
okText={t("contracts.actions.senddltoform")}
onOk={handleInsertForm}
okButtonProps={{ disabled: !!!decodedBarcode }}
onCancel={handleCancel}
>
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 (
<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")}>
{moment(
`20${decodedBarcode.expiration_date}${moment(
decodedBarcode.birthday
).format("DD")}`
).format("MM/DD/YYYY")}
</DataLabel>
<DataLabel label={t("contracts.fields.driver_dob")}>
{moment(decodedBarcode.birthday).format("MM/DD/YYYY")}
</DataLabel>
<Modal
open={modalVisible}
okText={t("contracts.actions.senddltoform")}
onOk={handleInsertForm}
okButtonProps={{disabled: !!!decodedBarcode}}
onCancel={handleCancel}
>
<div>
<Typography.Title level={4}>
{t("contracts.labels.correctdataonform")}
</Typography.Title>
<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>
</div>
</div>
) : (
<div>{t("contracts.labels.waitingforscan")}</div>
)}
</LoadingSkeleton>
</Modal>
<Button onClick={handleClick}>
{t("contracts.actions.decodelicense")}
</Button>
</div>
</Modal>
<Button onClick={handleClick}>
{t("contracts.actions.decodelicense")}
</Button>
</div>
);
);
}

View File

@@ -1,32 +1,33 @@
import React, { useState, useEffect, forwardRef } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
import React, {forwardRef, useEffect, useState} from "react";
import {Select} from "antd";
import {useTranslation} from "react-i18next";
const ContractStatusComponent = ({ value, onChange }, ref) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
const {Option} = Select;
useEffect(() => {
if (value !== option && onChange) {
onChange(option);
}
}, [value, option, onChange]);
const ContractStatusComponent = ({value, onChange}, ref) => {
const [option, setOption] = useState(value);
const {t} = useTranslation();
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>
);
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>
);
};
export default forwardRef(ContractStatusComponent);

View File

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

View File

@@ -1,169 +1,169 @@
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 { visible } = 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,
},
});
};
useEffect(() => {
if (visible) {
form.resetFields();
}
}, [visible, form]);
return (
<Modal
visible={visible}
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>
),
callSearch({
variables: {
plate:
(values.plate && values.plate !== "" && values.plate) || undefined,
time: values.time,
},
{
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>
);
});
};
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>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ContractsFindModalContainer);

View File

@@ -1,224 +1,224 @@
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, useHistory, useLocation } 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 moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
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";
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 = useHistory();
const search = queryString.parse(useLocation().search);
const { page } = search;
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 &&
`${moment(record.actualreturn)
.diff(moment(record.start), "days", 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.push({ 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.push({ 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"
}}
>
{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.push({ 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>
);
pagination={{
position: "top",
pageSize: pageLimit,
current: parseInt(page || 1),
total: total,
}}
columns={columns}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}
/>
</Card>
);
}

View File

@@ -1,46 +1,42 @@
import { DownOutlined } from "@ant-design/icons";
import { Dropdown, Menu } 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 menu = (
<div>
<Menu onClick={handleClick}>
{bodyshop.md_ccc_rates.map((rate, idx) => (
<Menu.Item value={rate} key={idx}>
{rate.label}
</Menu.Item>
))}
</Menu>
</div>
);
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({
key: idx,
label: i.label,
value: i,
}));
return (
<Dropdown overlay={menu} disabled={disabled}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
{t("contracts.actions.changerate")} <DownOutlined />
</a>
</Dropdown>
);
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>
);
}
export default connect(mapStateToProps, null)(ContractsRatesChangeButton);

View File

@@ -1,104 +1,104 @@
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, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
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 = useHistory();
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.push({ 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>
);
}

View File

@@ -1,11 +1,12 @@
import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import moment from "moment";
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";
@@ -14,352 +15,352 @@ 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>
}
/>
{/* <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();
return (
<div>
<PageHeader
title={t("menus.header.courtesycars")}
extra={
<Button
type="primary"
loading={saveLoading}
onClick={() => form.submit()}
>
{t("general.actions.save")}
</Button>
}
},
}),
]}
>
<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" }}>
{/* <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"}}>
<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 &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
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");
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 && moment(expires).endOf("day").isBefore(moment());
const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
if (dateover)
return (
<Space direction="vertical" style={{color: "tomato"}}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
<WarningFilled style={{marginRight: ".3rem"}}/>
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
</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");
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 && moment(expires).endOf("day").isBefore(moment());
const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
if (dateover)
return (
<Space direction="vertical" style={{color: "tomato"}}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
<WarningFilled style={{marginRight: ".3rem"}}/>
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
</Space>
);
return <></>;
}}
</Form.Item>
return <></>;
}}
</Form.Item>
</div>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput/>
</Form.Item>
</LayoutFormRow>
</div>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>
</LayoutFormRow>
</div>
);
);
}

View File

@@ -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>,
},
};
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;
}
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;
}
},
}}
/>
);
};
export default forwardRef(CourtesyCarFuelComponent);

View File

@@ -1,35 +1,36 @@
import { Select } from "antd";
import React, { forwardRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
import {Select} from "antd";
import React, {forwardRef, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
const {Option} = Select;
useEffect(() => {
if (value !== option && onChange) {
onChange(option);
}
}, [value, option, onChange]);
const CourtesyCarReadinessComponent = ({value, onChange}, ref) => {
const [option, setOption] = useState(value);
const {t} = useTranslation();
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>
);
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>
);
};
export default forwardRef(CourtesyCarReadinessComponent);

View File

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

View File

@@ -1,88 +1,88 @@
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 moment from "moment";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/client";
import dayjs from "../../utils/day";
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 { visible, 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);
};
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")}
visible={visible}
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: moment(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
mapStateToProps,
mapDispatchToProps
)(CCReturnModalContainer);

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