Merged in development (pull request #44)
Begin UI Changes for Production
This commit is contained in:
@@ -16045,6 +16045,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>ins_co_nm_short</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>ins_ct_fn</name>
|
<name>ins_ct_fn</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19702,6 +19723,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>estimatelines</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>existing_jobs</name>
|
<name>existing_jobs</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -55,19 +55,36 @@
|
|||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "^5.2.0",
|
||||||
"subscriptions-transport-ws": "^0.9.18"
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
|
"web-vitals": "^0.2.4",
|
||||||
|
"workbox-background-sync": "^5.1.3",
|
||||||
|
"workbox-broadcast-update": "^5.1.3",
|
||||||
|
"workbox-cacheable-response": "^5.1.3",
|
||||||
|
"workbox-core": "^5.1.3",
|
||||||
|
"workbox-expiration": "^5.1.3",
|
||||||
|
"workbox-google-analytics": "^5.1.3",
|
||||||
|
"workbox-navigation-preload": "^5.1.3",
|
||||||
|
"workbox-precaching": "^5.1.3",
|
||||||
|
"workbox-range-requests": "^5.1.3",
|
||||||
|
"workbox-routing": "^5.1.3",
|
||||||
|
"workbox-strategies": "^5.1.3",
|
||||||
|
"workbox-streams": "^5.1.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||||
|
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
||||||
"build-deploy": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && s3cmd sync build/* s3://imex-online-production && echo '🚀 Deployed!'",
|
"build-deploy": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && s3cmd sync build/* s3://imex-online-production && echo '🚀 Deployed!'",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function AppContainer() {
|
|||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
componentSize="small"
|
//componentSize="small"
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { lazy, Suspense, useEffect } from "react";
|
import React, { lazy, Suspense, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -48,48 +47,46 @@ export function App({ checkUserSession, currentUser }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Switch>
|
||||||
<Switch>
|
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
||||||
<Suspense fallback={<LoadingSpinner message="App.Js Suspense" />}>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route exact path="/" component={LandingPage} />
|
||||||
<Route exact path="/" component={LandingPage} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route exact path="/signin" component={SignInPage} />
|
||||||
<Route exact path="/signin" component={SignInPage} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route exact path="/resetpassword" component={ResetPassword} />
|
||||||
<Route exact path="/resetpassword" component={ResetPassword} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route exact path="/about" component={AboutPage} />
|
||||||
<Route exact path="/about" component={AboutPage} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<Route
|
||||||
<Route
|
exact
|
||||||
exact
|
path="/mp/:paymentIs"
|
||||||
path="/mp/:paymentIs"
|
component={MobilePaymentContainer}
|
||||||
component={MobilePaymentContainer}
|
/>
|
||||||
/>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<PrivateRoute
|
||||||
<PrivateRoute
|
isAuthorized={currentUser.authorized}
|
||||||
isAuthorized={currentUser.authorized}
|
path="/manage"
|
||||||
path="/manage"
|
component={ManagePage}
|
||||||
component={ManagePage}
|
/>
|
||||||
/>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<PrivateRoute
|
||||||
<PrivateRoute
|
isAuthorized={currentUser.authorized}
|
||||||
isAuthorized={currentUser.authorized}
|
path="/tech"
|
||||||
path="/tech"
|
component={TechPageContainer}
|
||||||
component={TechPageContainer}
|
/>
|
||||||
/>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
</Suspense>
|
</Switch>
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,17 +67,12 @@
|
|||||||
// background-color: #188fff;
|
// background-color: #188fff;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.ant-table-cell {
|
|
||||||
// background-color: red;
|
|
||||||
//padding: 0.2rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-number-input,
|
.ant-input-number-input,
|
||||||
.ant-input-number,
|
.ant-input-number,
|
||||||
.ant-picker-input,
|
.ant-picker-input,
|
||||||
.ant-picker,
|
.ant-picker,
|
||||||
.ant-select {
|
.ant-select {
|
||||||
width: 100%;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.production-alert {
|
.production-alert {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.breadcrumb-container {
|
.breadcrumb-container {
|
||||||
margin: 0.5rem 4rem;
|
margin: 0.2rem 0.2rem 0.8rem 0.2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MessageFilled } from "@ant-design/icons";
|
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
@@ -12,16 +12,13 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
if (!phone) return <></>;
|
||||||
return (
|
return (
|
||||||
<MessageFilled
|
<a
|
||||||
style={{ margin: 4 }}
|
href="# "
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: chat-open-button.component.jsx ~ line 21 ~ p",
|
|
||||||
p
|
|
||||||
);
|
|
||||||
|
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
||||||
@@ -29,7 +26,9 @@ export function ChatOpenButton({ phone, jobid, openChatByPhone }) {
|
|||||||
notification["error"]({ message: t("messaging.error.invalidphone") });
|
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
||||||
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(null, mapDispatchToProps)(ChatOpenButton);
|
export default connect(null, mapDispatchToProps)(ChatOpenButton);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Typography } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function DataLabel({
|
export default function DataLabel({
|
||||||
@@ -6,19 +7,34 @@ export default function DataLabel({
|
|||||||
children,
|
children,
|
||||||
vertical,
|
vertical,
|
||||||
visible = true,
|
visible = true,
|
||||||
|
valueStyle = {},
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
if (!visible || (hideIfNull && !!!children)) return null;
|
if (!visible || (hideIfNull && !!!children)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props} style={{ display: "flex" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: vertical ? "block" : "inline-block",
|
flex: 2,
|
||||||
marginRight: ".2rem",
|
marginRight: ".2rem",
|
||||||
}}>{`${label}: `}</div>
|
}}
|
||||||
<div style={{ display: vertical ? "block" : "inline-block" }}>
|
>
|
||||||
{children}
|
<Typography.Text type="secondary">{`${label}:`}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 4,
|
||||||
|
marginLeft: ".3rem",
|
||||||
|
fontWeight: "bolder",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof children === "string" ? (
|
||||||
|
<Typography.Text style={valueStyle}>{children}</Typography.Text>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ export default function FormsFieldChanged({ form }) {
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
const loc = useLocation();
|
const loc = useLocation();
|
||||||
|
if (!form.isFieldsTouched()) return <></>;
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item shouldUpdate style={{ margin: 0, padding: 0 }}>
|
||||||
shouldUpdate
|
|
||||||
//style={{ margin: 0, padding: 0 }}
|
|
||||||
>
|
|
||||||
{() => {
|
{() => {
|
||||||
if (form.isFieldsTouched())
|
if (form.isFieldsTouched())
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Icon, {
|
|||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Avatar, Menu } from "antd";
|
import { Menu } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
@@ -67,10 +67,10 @@ function Header({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignContent: "center" }}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
theme="dark"
|
theme="light"
|
||||||
style={{ flex: 5 }}
|
style={{ flex: 5 }}
|
||||||
selectedKeys={[selectedHeader]}
|
selectedKeys={[selectedHeader]}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
@@ -297,38 +297,15 @@ function Header({
|
|||||||
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
</Menu>
|
|
||||||
<GlobalSearch />
|
<Menu.Item>
|
||||||
<Menu
|
<GlobalSearch />
|
||||||
mode="horizontal"
|
</Menu.Item>
|
||||||
theme="dark"
|
|
||||||
selectedKeys={[selectedHeader]}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
>
|
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
title={
|
title={
|
||||||
<div>
|
currentUser.displayName ||
|
||||||
{currentUser.photoURL ? (
|
currentUser.email ||
|
||||||
<Avatar
|
t("general.labels.unknown")
|
||||||
src={currentUser.photoURL}
|
|
||||||
style={{
|
|
||||||
margin: "10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#87d068",
|
|
||||||
margin: "10px",
|
|
||||||
}}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentUser.displayName ||
|
|
||||||
currentUser.email ||
|
|
||||||
t("general.labels.unknown")}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item danger onClick={() => signOutStart()}>
|
<Menu.Item danger onClick={() => signOutStart()}>
|
||||||
|
|||||||
@@ -8,17 +8,8 @@ export default function JiraSupportComponent() {
|
|||||||
|
|
||||||
const useScript = () => {
|
const useScript = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Creating JIRA widget.");
|
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = "https://jsd-widget.atlassian.com/assets/embed.js";
|
script.src = "https://jsd-widget.atlassian.com/assets/embed.js";
|
||||||
// script["data-jsd-embedded"] = true;
|
|
||||||
// script["data-key"] = "d69bb65c-1dd3-483f-b109-66a970d03f44";
|
|
||||||
// script["data-base-url"] = "https://jsd-widget.atlassian.com";
|
|
||||||
|
|
||||||
// script.attributes.setNamedItem("data-jsd-embedded");
|
|
||||||
// script.attributes.setNamedItem("data-key");
|
|
||||||
// script.attributes.setNamedItem("data-base-url");
|
|
||||||
|
|
||||||
script.setAttribute("data-jsd-embedded", true);
|
script.setAttribute("data-jsd-embedded", true);
|
||||||
script.setAttribute("data-key", "d69bb65c-1dd3-483f-b109-66a970d03f44");
|
script.setAttribute("data-key", "d69bb65c-1dd3-483f-b109-66a970d03f44");
|
||||||
script.setAttribute("data-base-url", "https://jsd-widget.atlassian.com");
|
script.setAttribute("data-base-url", "https://jsd-widget.atlassian.com");
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
|
|||||||
|
|
||||||
let gppercentFormatted;
|
let gppercentFormatted;
|
||||||
if (isNaN(gppercent)) gppercentFormatted = "0%";
|
if (isNaN(gppercent)) gppercentFormatted = "0%";
|
||||||
else if (!isFinite(gppercent)) gppercentFormatted = "-∞";
|
else if (!isFinite(gppercent)) gppercentFormatted = "- ∞";
|
||||||
else {
|
else {
|
||||||
gppercentFormatted = `${gppercent}%`;
|
gppercentFormatted = `${gppercent}%`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Button, Col, Drawer, Grid, PageHeader, Row, Space, Tag } from "antd";
|
import { Button, Card, Drawer, Grid, PageHeader, Space, Tag } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -25,15 +25,6 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
const colBreakPoints = {
|
|
||||||
xs: {
|
|
||||||
span: 24,
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
span: 12,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function JobDetailCards({ setPrintCenterContext }) {
|
export function JobDetailCards({ setPrintCenterContext }) {
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
@@ -69,20 +60,23 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const gridStyle = {
|
||||||
|
width: "25%",
|
||||||
|
textAlign: "center",
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
visible={!!selected}
|
visible={!!selected}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
width={drawerPercentage}
|
height={drawerPercentage}
|
||||||
placement="right"
|
placement="top"
|
||||||
onClose={handleDrawerClose}
|
onClose={handleDrawerClose}
|
||||||
>
|
>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
ghost={true}
|
// ghost={true}
|
||||||
tags={[
|
tags={[
|
||||||
<OwnerTagPopoverComponent key="owner" job={data.jobs_by_pk} />,
|
<OwnerTagPopoverComponent key="owner" job={data.jobs_by_pk} />,
|
||||||
<VehicleTagPopoverComponent key="vehicle" job={data.jobs_by_pk} />,
|
<VehicleTagPopoverComponent key="vehicle" job={data.jobs_by_pk} />,
|
||||||
@@ -99,79 +93,80 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
{t("jobs.labels.inproduction")}
|
{t("jobs.labels.inproduction")}
|
||||||
</Tag>,
|
</Tag>,
|
||||||
]}
|
]}
|
||||||
title={
|
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
|
||||||
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
subTitle={data.jobs_by_pk.status}
|
subTitle={data.jobs_by_pk.status}
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setPrintCenterContext({
|
|
||||||
actions: { refetch: refetch },
|
|
||||||
context: {
|
|
||||||
id: data.jobs_by_pk.id,
|
|
||||||
job: data.jobs_by_pk,
|
|
||||||
type: "job",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
|
||||||
</Button>
|
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
|
||||||
<Button>{t("parts.actions.order")}</Button>
|
|
||||||
</Link>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Row gutter={[16, 16]}>
|
<Card
|
||||||
<Col {...colBreakPoints}>
|
title={
|
||||||
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
||||||
|
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPrintCenterContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
id: data.jobs_by_pk.id,
|
||||||
|
job: data.jobs_by_pk,
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PrinterFilled />
|
||||||
|
{t("jobs.actions.printCenter")}
|
||||||
|
</Button>
|
||||||
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
||||||
|
<Button>{t("parts.actions.order")}</Button>
|
||||||
|
</Link>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsInsuranceComponent
|
<JobDetailCardsInsuranceComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsTotalsComponent
|
<JobDetailCardsTotalsComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsDatesComponent
|
<JobDetailCardsDatesComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsPartsComponent
|
<JobDetailCardsPartsComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsNotesComponent
|
<JobDetailCardsNotesComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsDocumentsComponent
|
<JobDetailCardsDocumentsComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
<Col {...colBreakPoints}>
|
<Card.Grid style={gridStyle}>
|
||||||
<JobDetailCardsDamageComponent
|
<JobDetailCardsDamageComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card.Grid>
|
||||||
</Row>
|
</Card>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled, FilterFilled, SyncOutlined } from "@ant-design/icons";
|
import { DeleteFilled, FilterFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Dropdown, Input, Menu, Space, Table } from "antd";
|
import { Button, Dropdown, Input, Menu, PageHeader, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -39,35 +39,13 @@ export function JobLinesComponent({
|
|||||||
refetch,
|
refetch,
|
||||||
jobLines,
|
jobLines,
|
||||||
setSearchText,
|
setSearchText,
|
||||||
selectedLines,
|
|
||||||
setSelectedLines,
|
|
||||||
job,
|
job,
|
||||||
setJobLineEditContext,
|
setJobLineEditContext,
|
||||||
form,
|
form,
|
||||||
}) {
|
}) {
|
||||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||||
|
|
||||||
// const {
|
const [selectedLines, setSelectedLines] = useState([]);
|
||||||
// loading: billLinesLoading,
|
|
||||||
// error: billLinesError,
|
|
||||||
// data: billLinesData,
|
|
||||||
// } = useQuery(QUERY_BILLS_BY_JOB_REF, {
|
|
||||||
// variables: { jobId: job && job.id },
|
|
||||||
// skip: loading || !job,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const billLinesDataObj = useMemo(() => {
|
|
||||||
// if (!billLinesData) return {};
|
|
||||||
// const ret = {};
|
|
||||||
// billLinesData.billlines.map((b) => {
|
|
||||||
// if (b.joblineid) {
|
|
||||||
// ret[b.joblineid] = { ...b, total: b.actual_price * b.quantity };
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
// });
|
|
||||||
// return ret;
|
|
||||||
// }, [billLinesData]);
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: {},
|
filteredInfo: {},
|
||||||
@@ -80,6 +58,7 @@ export function JobLinesComponent({
|
|||||||
dataIndex: "line_no",
|
dataIndex: "line_no",
|
||||||
key: "line_no",
|
key: "line_no",
|
||||||
sorter: (a, b) => a.line_no - b.line_no,
|
sorter: (a, b) => a.line_no - b.line_no,
|
||||||
|
fixed: "left",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -87,13 +66,16 @@ export function JobLinesComponent({
|
|||||||
title: t("joblines.fields.line_ind"),
|
title: t("joblines.fields.line_ind"),
|
||||||
dataIndex: "line_ind",
|
dataIndex: "line_ind",
|
||||||
key: "line_ind",
|
key: "line_ind",
|
||||||
|
fixed: "left",
|
||||||
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
|
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
||||||
|
responsive: ["md"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.line_desc"),
|
title: t("joblines.fields.line_desc"),
|
||||||
dataIndex: "line_desc",
|
dataIndex: "line_desc",
|
||||||
|
fixed: "left",
|
||||||
key: "line_desc",
|
key: "line_desc",
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
@@ -231,6 +213,7 @@ export function JobLinesComponent({
|
|||||||
dataIndex: "billref",
|
dataIndex: "billref",
|
||||||
key: "billref",
|
key: "billref",
|
||||||
render: (text, record) => <JobLinesBillRefernece jobline={record} />,
|
render: (text, record) => <JobLinesBillRefernece jobline={record} />,
|
||||||
|
responsive: ["md"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.status"),
|
title: t("joblines.fields.status"),
|
||||||
@@ -258,35 +241,6 @@ export function JobLinesComponent({
|
|||||||
<JobLineStatusPopup jobline={record} disabled={jobRO} />
|
<JobLineStatusPopup jobline={record} disabled={jobRO} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: t("allocations.fields.employee"),
|
|
||||||
// dataIndex: "employee",
|
|
||||||
// key: "employee",
|
|
||||||
// sorter: (a, b) =>
|
|
||||||
// alphaSort(
|
|
||||||
// a.allocations[0] &&
|
|
||||||
// a.allocations[0].employee.first_name +
|
|
||||||
// a.allocations[0].employee.last_name,
|
|
||||||
// b.allocations[0] &&
|
|
||||||
// b.allocations[0].employee.first_name +
|
|
||||||
// b.allocations[0].employee.last_name
|
|
||||||
// ),
|
|
||||||
// sortOrder:
|
|
||||||
// state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
|
|
||||||
// render: (text, record) => (
|
|
||||||
// <span>
|
|
||||||
// {record.allocations && record.allocations.length > 0
|
|
||||||
// ? record.allocations.map((item) => (
|
|
||||||
// <AllocationsEmployeeLabelContainer
|
|
||||||
// key={item.id}
|
|
||||||
// refetch={refetch}
|
|
||||||
// allocation={item}
|
|
||||||
// />
|
|
||||||
// ))
|
|
||||||
// : null}
|
|
||||||
// </span>
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
@@ -330,14 +284,6 @@ export function JobLinesComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{
|
|
||||||
// <AllocationsAssignmentContainer
|
|
||||||
// key={record.id}
|
|
||||||
// refetch={refetch}
|
|
||||||
// jobLineId={record.id}
|
|
||||||
// hours={record.mod_lb_hrs}
|
|
||||||
// />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -366,109 +312,82 @@ export function JobLinesComponent({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PartsOrderModalContainer />
|
<PartsOrderModalContainer />
|
||||||
|
<PageHeader
|
||||||
|
title={t("jobs.labels.estimatelines")}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Button onClick={() => refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
(job && !job.converted) ||
|
||||||
|
(selectedLines.length > 0 ? false : true) ||
|
||||||
|
jobRO
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setPartsOrderContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
jobId: job.id,
|
||||||
|
linesToOrder: selectedLines,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Clear out the selected lines. IO-785
|
||||||
|
setSelectedLines([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("parts.actions.order")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
filteredInfo: {
|
||||||
|
part_type: ["PAN,PAL,PAA,PAS,PASL"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
|
||||||
|
</Button>
|
||||||
|
<Dropdown overlay={markMenu} trigger={["click"]}>
|
||||||
|
<Button>{t("jobs.actions.mark")}</Button>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={jobRO}
|
||||||
|
onClick={() => {
|
||||||
|
setJobLineEditContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: { jobid: job.id },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("joblines.actions.new")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="small"
|
|
||||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
dataSource={jobLines}
|
dataSource={jobLines}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
scroll={{
|
scroll={{
|
||||||
x: true,
|
x: true,
|
||||||
//y: "40rem"
|
|
||||||
}}
|
}}
|
||||||
title={() => {
|
|
||||||
return (
|
|
||||||
<div className="imex-table-header">
|
|
||||||
<Button onClick={() => refetch()}>
|
|
||||||
<SyncOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
(job && !job.converted) ||
|
|
||||||
(selectedLines.length > 0 ? false : true) ||
|
|
||||||
jobRO
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setPartsOrderContext({
|
|
||||||
actions: { refetch: refetch },
|
|
||||||
context: {
|
|
||||||
jobId: job.id,
|
|
||||||
linesToOrder: selectedLines,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//Clear out the selected lines. IO-785
|
|
||||||
setSelectedLines([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("parts.actions.order")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
filteredInfo: {
|
|
||||||
part_type: ["PAN,PAL,PAA,PAS,PASL"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
|
|
||||||
</Button>
|
|
||||||
<Dropdown overlay={markMenu} trigger={["click"]}>
|
|
||||||
<Button>{t("jobs.actions.mark")}</Button>
|
|
||||||
</Dropdown>
|
|
||||||
{
|
|
||||||
// <AllocationsBulkAssignmentContainer
|
|
||||||
// jobLines={selectedLines}
|
|
||||||
// refetch={refetch}
|
|
||||||
// />
|
|
||||||
}
|
|
||||||
<Button
|
|
||||||
disabled={jobRO}
|
|
||||||
onClick={() => {
|
|
||||||
setJobLineEditContext({
|
|
||||||
actions: { refetch: refetch },
|
|
||||||
context: { jobid: job.id },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("joblines.actions.new")}
|
|
||||||
</Button>
|
|
||||||
<div className="imex-table-header__search">
|
|
||||||
<Input.Search
|
|
||||||
placeholder={t("general.labels.search")}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSearchText(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
// expandedRowRender={(record) => (
|
|
||||||
// <div>
|
|
||||||
// <strong>{t("parts_orders.labels.orderhistory")}</strong>
|
|
||||||
// {record.parts_order_lines.map((item) => (
|
|
||||||
// <div key={item.id}>
|
|
||||||
// <Link
|
|
||||||
// to={`/manage/jobs/${job.id}?tab=partssublet&partsorderid=${item.parts_order.id}`}
|
|
||||||
// >
|
|
||||||
// {item.parts_order.order_number || ""}
|
|
||||||
// </Link>
|
|
||||||
// -
|
|
||||||
// <Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
|
|
||||||
// {item.parts_order.vendor.name || ""}
|
|
||||||
// </Link>
|
|
||||||
// {` on ${item.parts_order.order_date || ""}`}
|
|
||||||
// </div>
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// )}
|
|
||||||
rowClassName="table-small-margin"
|
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
selectedRowKeys: selectedLines.map((item) => item.id),
|
selectedRowKeys: selectedLines.map((item) => item.id),
|
||||||
onSelectAll: (selected, selectedRows, changeRows) => {
|
onSelectAll: (selected, selectedRows, changeRows) => {
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import JobLinesComponent from "./job-lines.component";
|
import JobLinesComponent from "./job-lines.component";
|
||||||
|
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||||
function JobLinesContainer({ job, joblines, refetch, form }) {
|
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [selectedLines, setSelectedLines] = useState([]);
|
|
||||||
|
|
||||||
const jobLines = joblines
|
const jobLines = useMemo(() => {
|
||||||
? searchText
|
return joblines
|
||||||
? joblines.filter(
|
? searchText
|
||||||
(jl) =>
|
? joblines.filter(
|
||||||
(jl.unq_seq || "")
|
(jl) =>
|
||||||
.toString()
|
(jl.unq_seq || "")
|
||||||
.toLowerCase()
|
.toString()
|
||||||
.includes(searchText.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(jl.line_desc || "")
|
.includes(searchText.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(jl.line_desc || "")
|
||||||
.includes(searchText.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(jl.part_type || "")
|
.includes(searchText.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(jl.part_type || "")
|
||||||
.includes(searchText.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(jl.oem_partno || "")
|
.includes(searchText.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(jl.oem_partno || "")
|
||||||
.includes(searchText.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(jl.op_code_desc || "")
|
.includes(searchText.toLowerCase()) ||
|
||||||
.toLowerCase()
|
(jl.op_code_desc || "")
|
||||||
.includes(searchText.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(jl.db_price || "").toString().includes(searchText.toLowerCase()) ||
|
.includes(searchText.toLowerCase()) ||
|
||||||
(jl.act_price || "").toString().includes(searchText.toLowerCase())
|
(jl.db_price || "")
|
||||||
)
|
.toString()
|
||||||
: joblines
|
.includes(searchText.toLowerCase()) ||
|
||||||
: null;
|
(jl.act_price || "").toString().includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: joblines
|
||||||
|
: [];
|
||||||
|
}, [joblines, searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JobLinesComponent
|
<JobLinesComponent
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
jobLines={jobLines}
|
jobLines={jobLines}
|
||||||
setSearchText={setSearchText}
|
setSearchText={setSearchText}
|
||||||
selectedLines={selectedLines}
|
|
||||||
setSelectedLines={setSelectedLines}
|
|
||||||
job={job}
|
job={job}
|
||||||
form={form}
|
form={form}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { DeleteFilled, PlusCircleFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Popover, Select, Spin } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PlusCircleFilled, MinusOutlined } from "@ant-design/icons";
|
|
||||||
import { Select, Button, Popover } from "antd";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -15,6 +16,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const iconStyle = { marginLeft: ".3rem" };
|
||||||
|
|
||||||
export function JobEmployeeAssignments({
|
export function JobEmployeeAssignments({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobRO,
|
jobRO,
|
||||||
@@ -23,6 +26,7 @@ export function JobEmployeeAssignments({
|
|||||||
prep,
|
prep,
|
||||||
handleAdd,
|
handleAdd,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
|
loading,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [assignment, setAssignment] = useState({
|
const [assignment, setAssignment] = useState({
|
||||||
@@ -68,93 +72,84 @@ export function JobEmployeeAssignments({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Popover destroyTooltipOnHide content={popContent} visible={visibility}>
|
||||||
<Popover destroyTooltipOnHide content={popContent} visible={visibility}>
|
<Spin spinning={loading}>
|
||||||
<div style={{ display: "flex" }}>
|
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||||
<DataLabel
|
{body ? (
|
||||||
label={t("jobs.fields.employee_body")}
|
<div>
|
||||||
style={{ margin: "0rem .5rem" }}
|
<span>{`${body.first_name || ""} ${body.last_name || ""}`}</span>
|
||||||
>
|
<DeleteFilled
|
||||||
{body ? (
|
operation="body"
|
||||||
<div>
|
|
||||||
<span>{`${body.first_name || ""} ${
|
|
||||||
body.last_name || ""
|
|
||||||
}`}</span>
|
|
||||||
<MinusOutlined
|
|
||||||
operation="body"
|
|
||||||
disabled={jobRO}
|
|
||||||
onClick={() => !jobRO && handleRemove("body")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PlusCircleFilled
|
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
onClick={() => {
|
style={iconStyle}
|
||||||
if (!jobRO) {
|
onClick={() => !jobRO && handleRemove("body")}
|
||||||
setAssignment({ operation: "body" });
|
|
||||||
setVisibility(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</DataLabel>
|
) : (
|
||||||
<DataLabel
|
<PlusCircleFilled
|
||||||
label={t("jobs.fields.employee_prep")}
|
disabled={jobRO}
|
||||||
style={{ margin: "0rem .5rem" }}
|
style={iconStyle}
|
||||||
>
|
onClick={() => {
|
||||||
{prep ? (
|
if (!jobRO) {
|
||||||
<div>
|
setAssignment({ operation: "body" });
|
||||||
<span>{`${prep.first_name || ""} ${
|
setVisibility(true);
|
||||||
prep.last_name || ""
|
}
|
||||||
}`}</span>
|
}}
|
||||||
<MinusOutlined
|
/>
|
||||||
disabled={jobRO}
|
)}
|
||||||
operation="prep"
|
</DataLabel>
|
||||||
onClick={() => !jobRO && handleRemove("prep")}
|
<DataLabel label={t("jobs.fields.employee_prep")}>
|
||||||
/>
|
{prep ? (
|
||||||
</div>
|
<div>
|
||||||
) : (
|
<span>{`${prep.first_name || ""} ${prep.last_name || ""}`}</span>
|
||||||
<PlusCircleFilled
|
<DeleteFilled
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
onClick={() => {
|
style={iconStyle}
|
||||||
if (!jobRO) {
|
operation="prep"
|
||||||
setAssignment({ operation: "prep" });
|
onClick={() => !jobRO && handleRemove("prep")}
|
||||||
setVisibility(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</DataLabel>
|
) : (
|
||||||
<DataLabel
|
<PlusCircleFilled
|
||||||
label={t("jobs.fields.employee_refinish")}
|
disabled={jobRO}
|
||||||
style={{ margin: "0rem .5rem" }}
|
style={iconStyle}
|
||||||
>
|
onClick={() => {
|
||||||
{refinish ? (
|
if (!jobRO) {
|
||||||
<div>
|
setAssignment({ operation: "prep" });
|
||||||
<span>{`${refinish.first_name || ""} ${
|
setVisibility(true);
|
||||||
refinish.last_name || ""
|
}
|
||||||
}`}</span>
|
}}
|
||||||
<MinusOutlined
|
/>
|
||||||
disabled={jobRO}
|
)}
|
||||||
operation="refinish"
|
</DataLabel>
|
||||||
onClick={() => !jobRO && handleRemove("refinish")}
|
<DataLabel label={t("jobs.fields.employee_refinish")}>
|
||||||
/>
|
{refinish ? (
|
||||||
</div>
|
<div>
|
||||||
) : (
|
<span>{`${refinish.first_name || ""} ${
|
||||||
<PlusCircleFilled
|
refinish.last_name || ""
|
||||||
|
}`}</span>
|
||||||
|
<DeleteFilled
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
onClick={() => {
|
style={iconStyle}
|
||||||
if (!jobRO) {
|
operation="refinish"
|
||||||
setAssignment({ operation: "refinish" });
|
onClick={() => !jobRO && handleRemove("refinish")}
|
||||||
setVisibility(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</DataLabel>
|
) : (
|
||||||
</div>
|
<PlusCircleFilled
|
||||||
</Popover>
|
disabled={jobRO}
|
||||||
</div>
|
style={iconStyle}
|
||||||
|
onClick={() => {
|
||||||
|
if (!jobRO) {
|
||||||
|
setAssignment({ operation: "refinish" });
|
||||||
|
setVisibility(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DataLabel>
|
||||||
|
</Spin>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
|
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleAdd = async (assignment) => {
|
const handleAdd = async (assignment) => {
|
||||||
|
setLoading(true);
|
||||||
const { operation, employeeid } = assignment;
|
const { operation, employeeid } = assignment;
|
||||||
logImEXEvent("job_assign_employee", { operation });
|
logImEXEvent("job_assign_employee", { operation });
|
||||||
|
|
||||||
@@ -30,8 +32,10 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
const handleRemove = async (operation) => {
|
const handleRemove = async (operation) => {
|
||||||
|
setLoading(true);
|
||||||
logImEXEvent("job_unassign_employee", { operation });
|
logImEXEvent("job_unassign_employee", { operation });
|
||||||
|
|
||||||
let empAssignment = determineFieldName(operation);
|
let empAssignment = determineFieldName(operation);
|
||||||
@@ -48,6 +52,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,6 +63,7 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
prep={job.employee_prep_rel}
|
prep={job.employee_prep_rel}
|
||||||
handleAdd={handleAdd}
|
handleAdd={handleAdd}
|
||||||
handleRemove={handleRemove}
|
handleRemove={handleRemove}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
205
client/src/components/job-payments/job-payments.component.jsx
Normal file
205
client/src/components/job-payments/job-payments.component.jsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { Button, Card, Space, Table } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setPaymentContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
|
||||||
|
|
||||||
|
export function JobPayments({
|
||||||
|
job,
|
||||||
|
jobRO,
|
||||||
|
bodyshop,
|
||||||
|
setPaymentContext,
|
||||||
|
refetch,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("payments.fields.created_at"),
|
||||||
|
dataIndex: "created_at",
|
||||||
|
key: "created_at",
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.payer"),
|
||||||
|
dataIndex: "payer",
|
||||||
|
key: "payer",
|
||||||
|
sorter: (a, b) => alphaSort(a.payer, b.payer),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "payer" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.amount"),
|
||||||
|
dataIndex: "amount",
|
||||||
|
key: "amount",
|
||||||
|
sorter: (a, b) => a.amount - b.amount,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.memo"),
|
||||||
|
dataIndex: "memo",
|
||||||
|
key: "memo",
|
||||||
|
sorter: (a, b) => alphaSort(a.memo, b.memo),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.type"),
|
||||||
|
dataIndex: "type",
|
||||||
|
key: "type",
|
||||||
|
sorter: (a, b) => alphaSort(a.type, b.type),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "type" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.transactionid"),
|
||||||
|
dataIndex: "transactionid",
|
||||||
|
key: "transactionid",
|
||||||
|
sorter: (a, b) => alphaSort(a.transactionid, b.transactionid),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "transactionid" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.stripeid"),
|
||||||
|
dataIndex: "stripeid",
|
||||||
|
key: "stripeid",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.stripeid ? (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
stripeTestEnv
|
||||||
|
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${record.stripeid}`
|
||||||
|
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${record.stripeid}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.stripeid}
|
||||||
|
</a>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
render: (text, record) => (
|
||||||
|
<PrintWrapperComponent
|
||||||
|
templateObject={{
|
||||||
|
name: TemplateList("payment").payment_receipt.key,
|
||||||
|
variables: { id: record.id },
|
||||||
|
}}
|
||||||
|
messageObject={{
|
||||||
|
to: job.ownr_ea,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const total = useMemo(() => {
|
||||||
|
return (
|
||||||
|
job.payments &&
|
||||||
|
job.payments.reduce((acc, val) => {
|
||||||
|
acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) }));
|
||||||
|
return acc;
|
||||||
|
}, Dinero())
|
||||||
|
);
|
||||||
|
}, [job.payments]);
|
||||||
|
|
||||||
|
const balance = useMemo(() => {
|
||||||
|
if (job && job.job_totals && job.job_totals.totals.total_repairs)
|
||||||
|
return Dinero(job.job_totals.totals.total_repairs).subtract(total);
|
||||||
|
return Dinero({ amount: 0 }).subtract(total);
|
||||||
|
}, [job, total]);
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("payments.labels.title")}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
disabled={jobRO}
|
||||||
|
onClick={() =>
|
||||||
|
setPaymentContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: { jobid: job.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("menus.header.enterpayment")}
|
||||||
|
</Button>
|
||||||
|
<DataLabel
|
||||||
|
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||||
|
label={t("payments.labels.balance")}
|
||||||
|
>
|
||||||
|
{balance.toFormat()}
|
||||||
|
</DataLabel>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={job && job.payments}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>{t("payments.labels.totalpayments")}</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>{total.toFormat()}</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
</Table.Summary.Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobPayments);
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Col, Collapse, Result, Row, Typography } from "antd";
|
import { Card, Col, Collapse, Result, Row } from "antd";
|
||||||
import Dinero from "dinero.js";
|
|
||||||
//import { JsonEditor as Editor } from "jsoneditor-react";
|
//import { JsonEditor as Editor } from "jsoneditor-react";
|
||||||
//import "jsoneditor-react/es/editor.min.css";
|
//import "jsoneditor-react/es/editor.min.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -7,14 +6,16 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
|
import JobCalculateTotals from "../job-calculate-totals/job-calculate-totals.component";
|
||||||
import "./job-totals-table.styles.scss";
|
import "./job-totals-table.styles.scss";
|
||||||
|
import JobTotalsTableLabor from "./job-totals.table.labor.component";
|
||||||
|
import JobTotalsTableOther from "./job-totals.table.other.component";
|
||||||
|
import JobTotalsTableParts from "./job-totals.table.parts.component";
|
||||||
|
import JobTotalsTableTotals from "./job-totals.table.totals.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const colSpan = {
|
const colSpan = {
|
||||||
@@ -22,15 +23,17 @@ const colSpan = {
|
|||||||
lg: { span: 12 },
|
lg: { span: 12 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
|
export function JobsTotalsTableComponent({ jobRO, job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!!!job.job_totals) {
|
if (!!!job.job_totals) {
|
||||||
return (
|
return (
|
||||||
<Result
|
<Card>
|
||||||
title={t("jobs.errors.nofinancial")}
|
<Result
|
||||||
extra={<JobCalculateTotals job={job} disabled={jobRO} />}
|
title={t("jobs.errors.nofinancial")}
|
||||||
/>
|
extra={<JobCalculateTotals job={job} disabled={jobRO} />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,334 +41,50 @@ export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
|
|||||||
<div>
|
<div>
|
||||||
<Row gutter={[32, 32]}>
|
<Row gutter={[32, 32]}>
|
||||||
<Col {...colSpan}>
|
<Col {...colSpan}>
|
||||||
<div className="job-totals-half">
|
<Card title={t("jobs.labels.labortotals")}>
|
||||||
<Typography.Title level={4}>
|
<JobTotalsTableLabor job={job} />
|
||||||
{t("jobs.labels.labortotals")}
|
</Card>
|
||||||
</Typography.Title>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_laa")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.laa.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.laa.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.laa.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lab")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lab.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lab.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lab.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lad")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lad.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lad.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lad.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lae")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lae.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lae.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lae.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_laf")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.laf.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.laf.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.laf.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lag")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lag.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lag.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lag.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lam")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lam.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lam.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lam.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lar")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lar.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lar.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lar.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_las")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.las.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.las.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.las.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_lau")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.lau.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.lau.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.lau.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_la1")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.la1.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.la1.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.la1.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_la2")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.la2.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.la2.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.la2.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_la3")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.la3.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.la3.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.la3.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.rate_la4")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.la4.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.la4.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.la4.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.labor_rates_subtotal")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
<strong>
|
|
||||||
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
|
|
||||||
</strong>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.mapa")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.mapa.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.mapa.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.mash")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
<td>{`(${job.job_totals.rates.mash.hours.toFixed(2)} @ ${
|
|
||||||
job.job_totals.rates.mash.rate
|
|
||||||
})`}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.rates_subtotal")}</td>
|
|
||||||
|
|
||||||
<td className="currency">
|
|
||||||
<strong>
|
|
||||||
{Dinero(job.job_totals.rates.subtotal).toFormat()}
|
|
||||||
</strong>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...colSpan}>
|
<Col {...colSpan}>
|
||||||
<div className="job-totals-half">
|
<Row gutter={[0, 32]}>
|
||||||
<Typography.Title level={4}>
|
<Col span={24}>
|
||||||
{t("jobs.labels.partstotal")}
|
<Card title={t("jobs.labels.partstotal")}>
|
||||||
</Typography.Title>
|
<JobTotalsTableParts job={job} />
|
||||||
<table>
|
</Card>
|
||||||
<tbody>
|
</Col>
|
||||||
{Object.keys(job.job_totals.parts.parts.list).map(
|
<Col span={24}>
|
||||||
(key, idx) => (
|
<Card title={t("jobs.labels.othertotal")}>
|
||||||
<tr key={idx}>
|
<JobTotalsTableOther job={job} />
|
||||||
<td>{t(`jobs.fields.${key.toLowerCase()}`)}</td>
|
</Card>
|
||||||
<td className="currency">
|
</Col>
|
||||||
{Dinero(
|
<Col span={24}>
|
||||||
job.job_totals.parts.parts.list[key].total
|
<Card title={t("jobs.labels.jobtotals")}>
|
||||||
).toFormat()}
|
<JobTotalsTableTotals job={job} />
|
||||||
</td>
|
</Card>
|
||||||
</tr>
|
</Col>
|
||||||
)
|
<Col span={24}>
|
||||||
)}
|
<Card title="DEVELOPMENT USE ONLY">
|
||||||
<tr>
|
<JobCalculateTotals job={job} disabled={jobRO} />
|
||||||
<td>{t("jobs.labels.partstotal")}</td>
|
<Collapse>
|
||||||
<td className="currency">
|
<Collapse.Panel header="JSON Tree Totals">
|
||||||
<strong>
|
<div>
|
||||||
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
<pre>
|
||||||
</strong>
|
{JSON.stringify(
|
||||||
</td>
|
{
|
||||||
</tr>
|
CIECA: job.cieca_ttl && job.cieca_ttl.data,
|
||||||
</tbody>
|
CIECASTL: job.cieca_stl && job.cieca_stl.data,
|
||||||
</table>
|
ImEXCalc: job.job_totals,
|
||||||
|
},
|
||||||
<Typography.Title level={4}>
|
null,
|
||||||
{t("jobs.labels.othertotal")}
|
2
|
||||||
</Typography.Title>
|
)}
|
||||||
<table>
|
</pre>
|
||||||
<tbody>
|
</div>
|
||||||
<tr>
|
</Collapse.Panel>
|
||||||
<td>{t("jobs.labels.subletstotal")}</td>
|
</Collapse>
|
||||||
<td className="currency">
|
</Card>
|
||||||
{Dinero(job.job_totals.parts.sublets.total).toFormat()}
|
</Col>
|
||||||
</td>
|
</Row>
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.additionaltotal")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.additional).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<Typography.Title level={4}>
|
|
||||||
{t("jobs.labels.jobtotals")}
|
|
||||||
</Typography.Title>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.subtotal")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
<strong>
|
|
||||||
{Dinero(job.job_totals.totals.subtotal).toFormat()}
|
|
||||||
</strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.local_tax_amt")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.totals.local_tax).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.state_tax_amt")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.totals.state_tax).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.federal_tax_amt")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.totals.federal_tax).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.ded_amt")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(
|
|
||||||
job.job_totals.totals.custPayable.deductible
|
|
||||||
).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.federal_tax_payable")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(
|
|
||||||
job.job_totals.totals.custPayable.federal_tax
|
|
||||||
).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.other_amount_payable")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(
|
|
||||||
job.job_totals.totals.custPayable.other_customer_amount
|
|
||||||
).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.fields.depreciation_taxes")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(
|
|
||||||
job.job_totals.totals.custPayable.dep_taxes
|
|
||||||
).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.total_repairs")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.totals.total_repairs).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.total_cust_payable")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
{Dinero(job.job_totals.totals.custPayable.total).toFormat()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{t("jobs.labels.net_repairs")}</td>
|
|
||||||
<td className="currency">
|
|
||||||
<strong>
|
|
||||||
{Dinero(job.job_totals.totals.net_repairs).toFormat()}
|
|
||||||
</strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<JobCalculateTotals job={job} disabled={jobRO} />
|
|
||||||
<Collapse>
|
|
||||||
<Collapse.Panel header="JSON Tree Totals">
|
|
||||||
<div>
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(
|
|
||||||
{
|
|
||||||
CIECA: job.cieca_ttl && job.cieca_ttl.data,
|
|
||||||
ImEXCalc: job.job_totals,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</Collapse.Panel>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
.job-totals-half {
|
// .job-totals-half {
|
||||||
flex: 1;
|
// flex: 1;
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
|
|
||||||
table {
|
// table {
|
||||||
border: 1px solid #ccc;
|
// border: 1px solid #ccc;
|
||||||
border-collapse: collapse;
|
// border-collapse: collapse;
|
||||||
margin: 0;
|
// margin: 0;
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
width: 80%;
|
// width: 80%;
|
||||||
table-layout: fixed;
|
// table-layout: fixed;
|
||||||
}
|
// }
|
||||||
|
|
||||||
table tr {
|
// table tr {
|
||||||
//background-color: #f8f8f8;
|
// //background-color: #f8f8f8;
|
||||||
border: 1px solid #ddd;
|
// border: 1px solid #ddd;
|
||||||
padding: 0.35em;
|
// padding: 0.35em;
|
||||||
}
|
// }
|
||||||
|
|
||||||
table th,
|
// table th,
|
||||||
table td {
|
// table td {
|
||||||
padding: 0.625em;
|
// padding: 0.625em;
|
||||||
//text-align: center;
|
// //text-align: center;
|
||||||
}
|
// }
|
||||||
table td.currency {
|
// table td.currency {
|
||||||
text-align: right;
|
// text-align: right;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
.job-totals-stats {
|
// .job-totals-stats {
|
||||||
margin: 1rem;
|
// margin: 1rem;
|
||||||
display: flex;
|
// display: flex;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
//flex-direction: column;
|
// //flex-direction: column;
|
||||||
justify-content: space-evenly;
|
// justify-content: space-evenly;
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function JobTotalsTableLabor({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {
|
||||||
|
columnKey: "profitcenter_labor",
|
||||||
|
field: "profitcenter_labor",
|
||||||
|
order: "ascend",
|
||||||
|
},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return Object.keys(job.job_totals.rates)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
key !== "mapa" &&
|
||||||
|
key !== "mash" &&
|
||||||
|
key !== "subtotal" &&
|
||||||
|
key !== "rates_subtotal"
|
||||||
|
)
|
||||||
|
.map((key) => {
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...job.job_totals.rates[key],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [job.job_totals.rates]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.profitcenter_labor"),
|
||||||
|
dataIndex: "profitcenter_labor",
|
||||||
|
key: "profitcenter_labor",
|
||||||
|
defaultSortOrder: "ascend",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
t(`jobs.fields.rate_${a.id.toLowerCase()}`),
|
||||||
|
t(`jobs.fields.rate_${b.id.toLowerCase()}`)
|
||||||
|
),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "profitcenter_labor" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
render: (text, record) =>
|
||||||
|
t(`jobs.fields.rate_${record.id.toLowerCase()}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.rates"),
|
||||||
|
dataIndex: "rate",
|
||||||
|
key: "rate",
|
||||||
|
align: "right",
|
||||||
|
sorter: (a, b) => a.rate - b.rate,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "rate" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.rate}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lb_hrs"),
|
||||||
|
dataIndex: "mod_lb_hrs",
|
||||||
|
|
||||||
|
key: "mod_lb_hrs",
|
||||||
|
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||||
|
|
||||||
|
render: (text, record) => record.hours.toFixed(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
align: "right",
|
||||||
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
|
||||||
|
render: (text, record) => Dinero(record.total).toFormat(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
console.log("sorter :>> ", sorter);
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={data}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{t("jobs.labels.labor_rates_subtotal")}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>
|
||||||
|
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
|
||||||
|
</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>{t("jobs.labels.mapa")}</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{job.job_totals.rates.mapa.rate}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{job.job_totals.rates.mapa.hours.toFixed(2)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>{t("jobs.labels.mash")}</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{job.job_totals.rates.mash.rate}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{job.job_totals.rates.mash.hours.toFixed(2)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{t("jobs.labels.labor_rates_subtotal")}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>
|
||||||
|
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
|
||||||
|
</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function JobTotalsTableOther({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.subletstotal"),
|
||||||
|
total: job.job_totals.parts.sublets.total,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.additionaltotal"),
|
||||||
|
total: job.job_totals.additional,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [job.job_totals, t]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
//title: t("joblines.fields.part_type"),
|
||||||
|
dataIndex: "key",
|
||||||
|
key: "key",
|
||||||
|
sorter: (a, b) => alphaSort(a.key, b.key),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "key" && state.sortedInfo.order,
|
||||||
|
width: "0%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
width: "20%",
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => Dinero(record.total).toFormat(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="key"
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={data}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{t("jobs.labels.additionaltotal")}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>
|
||||||
|
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||||
|
</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
|
export default function JobTotalsTableParts({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return Object.keys(job.job_totals.parts.parts.list)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
key !== "mapa" &&
|
||||||
|
key !== "mash" &&
|
||||||
|
key !== "subtotal" &&
|
||||||
|
key !== "rates_subtotal"
|
||||||
|
)
|
||||||
|
.map((key) => {
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
...job.job_totals.parts.parts.list[key],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [job.job_totals.parts.parts.list]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.part_type"),
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
t(`jobs.fields.${a.id.toLowerCase()}`),
|
||||||
|
t(`jobs.fields.${b.id.toLowerCase()}`)
|
||||||
|
),
|
||||||
|
width: "80%",
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "id" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => t(`jobs.fields.${record.id.toLowerCase()}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
width: "20%",
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => Dinero(record.total).toFormat(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={data}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>{t("jobs.labels.partstotal")}</Table.Summary.Cell>
|
||||||
|
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<strong>
|
||||||
|
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||||
|
</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function JobTotalsTableTotals({ job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.subtotal"),
|
||||||
|
total: job.job_totals.totals.subtotal,
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.local_tax_amt"),
|
||||||
|
total: job.job_totals.totals.local_tax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.state_tax_amt"),
|
||||||
|
total: job.job_totals.totals.state_tax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.federal_tax_amt"),
|
||||||
|
total: job.job_totals.totals.federal_tax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.fields.ded_amt"),
|
||||||
|
total: job.job_totals.totals.custPayable.deductible,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.fields.federal_tax_payable"),
|
||||||
|
total: job.job_totals.totals.custPayable.federal_tax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.fields.other_amount_payable"),
|
||||||
|
total: job.job_totals.totals.custPayable.other_customer_amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.fields.depreciation_taxes"),
|
||||||
|
total: job.job_totals.totals.custPayable.dep_taxes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.total_repairs"),
|
||||||
|
total: job.job_totals.totals.total_repairs,
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.total_cust_payable"),
|
||||||
|
total: job.job_totals.totals.custPayable.total,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.net_repairs"),
|
||||||
|
total: job.job_totals.totals.net_repairs,
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [job.job_totals, t]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
//title: t("joblines.fields.part_type"),
|
||||||
|
dataIndex: "key",
|
||||||
|
key: "key",
|
||||||
|
width: "80%",
|
||||||
|
onCell: (record, rowIndex) => {
|
||||||
|
return { style: { fontWeight: record.bold && "bold" } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
align: "right",
|
||||||
|
render: (text, record) => Dinero(record.total).toFormat(),
|
||||||
|
width: "20%",
|
||||||
|
onCell: (record, rowIndex) => {
|
||||||
|
return { style: { fontWeight: record.bold && "bold" } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="key"
|
||||||
|
showHeader={false}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={data}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -246,10 +246,6 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
const newTotals = await Axios.post("/job/totalsssu", {
|
const newTotals = await Axios.post("/job/totalsssu", {
|
||||||
id: selectedJob,
|
id: selectedJob,
|
||||||
});
|
});
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: jobs-available-table.container.jsx ~ line 247 ~ newTotals",
|
|
||||||
newTotals
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newTotals.status !== 200) {
|
if (newTotals.status !== 200) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Dropdown, Menu, notification } from "antd";
|
import { Button, Dropdown, Menu, notification } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useMutation } from "@apollo/client";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -90,14 +90,15 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="imex-flex-row__margin"
|
|
||||||
overlay={statusmenu}
|
overlay={statusmenu}
|
||||||
trigger={["click"]}
|
trigger={["click"]}
|
||||||
key="changestatus"
|
key="changestatus"
|
||||||
disabled={jobRO || !job.converted}
|
disabled={jobRO || !job.converted}
|
||||||
>
|
>
|
||||||
<Button>
|
<Button shape="round">
|
||||||
{t("jobs.actions.changestatus")} <DownCircleFilled />
|
<span>{job.status}</span>
|
||||||
|
|
||||||
|
<DownCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (job.converted) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover visible={visible} content={popMenu}>
|
<Popover visible={visible} content={popMenu}>
|
||||||
<Button
|
<Button
|
||||||
key="convert"
|
key="convert"
|
||||||
className="imex-flex-row__margin"
|
|
||||||
type="danger"
|
type="danger"
|
||||||
style={{ display: job.converted ? "none" : "" }}
|
// style={{ display: job.converted ? "none" : "" }}
|
||||||
disabled={job.converted || jobRO}
|
disabled={job.converted || jobRO}
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const lossColDamage = { sm: { span: 24 }, md: { span: 6 }, lg: { span: 4 } };
|
|||||||
export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||||
const { getFieldValue } = form;
|
const { getFieldValue } = form;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormRow header={t("jobs.forms.claiminfo")}>
|
<FormRow header={t("jobs.forms.claiminfo")}>
|
||||||
|
|||||||
@@ -390,14 +390,11 @@ export function JobsDetailHeaderActions({
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
|
||||||
className="imex-flex-row__margin"
|
|
||||||
overlay={statusmenu}
|
|
||||||
trigger={["click"]}
|
|
||||||
key="changestatus"
|
|
||||||
>
|
|
||||||
<Button>
|
<Button>
|
||||||
{t("general.labels.actions")} <DownCircleFilled />
|
<span>{t("general.labels.actions")}</span>
|
||||||
|
|
||||||
|
<DownCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { Card, Col, Row, Tag } from "antd";
|
||||||
import { Button, Divider, PageHeader, Tag } from "antd";
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
|
import DataLabel from "../data-label/data-label.component";
|
||||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||||
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
|
||||||
import JobsChangeStatus from "../jobs-change-status/jobs-change-status.component";
|
|
||||||
import JobsConvertButton from "../jobs-convert-button/jobs-convert-button.component";
|
|
||||||
import JobsDetailHeaderActions from "../jobs-detail-header-actions/jobs-detail-header-actions.component";
|
|
||||||
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
|
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
|
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -28,15 +24,25 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsDetailHeader({
|
const colSpan = {
|
||||||
setPrintCenterContext,
|
xs: {
|
||||||
jobRO,
|
span: 24,
|
||||||
job,
|
},
|
||||||
refetch,
|
sm: {
|
||||||
loading,
|
span: 24,
|
||||||
form,
|
},
|
||||||
bodyshop,
|
md: {
|
||||||
}) {
|
span: 12,
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
span: 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function JobsDetailHeader({ job, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const jobInPostProduction = useMemo(() => {
|
const jobInPostProduction = useMemo(() => {
|
||||||
@@ -45,80 +51,137 @@ export function JobsDetailHeader({
|
|||||||
);
|
);
|
||||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
||||||
|
|
||||||
const menuExtra = (
|
|
||||||
<div className="imex-flex-row">
|
|
||||||
<JobsChangeStatus job={job} />
|
|
||||||
<JobSyncButton job={job} />
|
|
||||||
<Button
|
|
||||||
className="imex-flex-row__margin"
|
|
||||||
onClick={() => {
|
|
||||||
setPrintCenterContext({
|
|
||||||
actions: { refetch: refetch },
|
|
||||||
context: {
|
|
||||||
id: job.id,
|
|
||||||
job: job,
|
|
||||||
type: "job",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
key="printing"
|
|
||||||
>
|
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
|
||||||
</Button>
|
|
||||||
<JobsConvertButton job={job} refetch={refetch} />
|
|
||||||
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
disabled={jobRO}
|
|
||||||
className="imex-flex-row__margin"
|
|
||||||
onClick={() => form.submit()}
|
|
||||||
>
|
|
||||||
{t("general.actions.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader
|
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||||
title={job.ro_number || t("general.labels.na")}
|
<Col {...colSpan}>
|
||||||
subTitle={job.status}
|
<Card title={"Job Status"} style={{ height: "100%" }}>
|
||||||
tags={[
|
<div>
|
||||||
<OwnerTagPopoverComponent key="owner" job={job} />,
|
<DataLabel label={t("jobs.fields.status")}>
|
||||||
<VehicleTagPopoverComponent key="vehicle" job={job} />,
|
{job.status}
|
||||||
<Tag
|
{job.inproduction && (
|
||||||
color="#f50"
|
<span style={{ marginLeft: ".5rem" }}>
|
||||||
key="production"
|
<Tag color="#f50" key="production">
|
||||||
style={{ display: job.inproduction ? "" : "none" }}
|
{t("jobs.labels.inproduction")}
|
||||||
>
|
</Tag>
|
||||||
{t("jobs.labels.inproduction")}
|
</span>
|
||||||
</Tag>,
|
)}
|
||||||
<Tag title={t("jobs.fields.repairtotal")} key="total" color="green">
|
</DataLabel>
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>
|
||||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
{job.ins_co_nm}
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
</DataLabel>
|
||||||
</Tag>,
|
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||||
]}
|
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||||
extra={menuExtra}
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
>
|
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
{(job.inproduction || jobInPostProduction) && (
|
</DataLabel>
|
||||||
<>
|
{(job.inproduction || jobInPostProduction) && (
|
||||||
<div style={{ display: "flex", flex: 1 }}>
|
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||||
<div style={{ marginRight: "2rem" }}>
|
<ProductionListColumnProductionNote record={job} />
|
||||||
{t("jobs.fields.production_vars.note")}
|
</DataLabel>
|
||||||
</div>
|
)}
|
||||||
<ProductionListColumnProductionNote record={job} />
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||||
|
<Card
|
||||||
|
className="ant-card-grid-hoverable"
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
||||||
|
job.ownr_co_nm || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||||
|
<ChatOpenButton>{job.ownr_ph1 || ""}</ChatOpenButton>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||||
|
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||||
|
job.ownr_city || ""
|
||||||
|
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||||
|
{job.ownr_ea || ""}
|
||||||
|
</DataLabel>
|
||||||
</div>
|
</div>
|
||||||
<Divider type="vertical" />
|
</Card>
|
||||||
</>
|
</Link>
|
||||||
)}
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
<JobEmployeeAssignments job={job} />
|
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
|
||||||
</div>
|
<Card
|
||||||
</PageHeader>
|
className="ant-card-grid-hoverable"
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={`${job.v_model_yr || ""} ${job.v_color || ""}
|
||||||
|
${job.v_make_desc || ""}
|
||||||
|
${job.v_model_desc || ""}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
||||||
|
{`${job.plate_no || t("general.labels.na")} (${`${
|
||||||
|
job.plate_st || t("general.labels.na")
|
||||||
|
}`})`}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||||
|
{`${job.v_vin || t("general.labels.na")}`}
|
||||||
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
<Col {...colSpan}>
|
||||||
|
<Card
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
title={t("jobs.labels.employeeassignments")}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<JobEmployeeAssignments job={job} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <PageHeader
|
||||||
|
// title={job.ro_number || t("general.labels.na")}
|
||||||
|
// subTitle={job.status}
|
||||||
|
// tags={[
|
||||||
|
// <OwnerTagPopoverComponent key="owner" job={job} />,
|
||||||
|
// <VehicleTagPopoverComponent key="vehicle" job={job} />,
|
||||||
|
// <Tag
|
||||||
|
// color="#f50"
|
||||||
|
// key="production"
|
||||||
|
// style={{ display: job.inproduction ? "" : "none" }}
|
||||||
|
// >
|
||||||
|
// {t("jobs.labels.inproduction")}
|
||||||
|
// </Tag>,
|
||||||
|
// <Tag title={t("jobs.fields.repairtotal")} key="total" color="green">
|
||||||
|
// <CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
|
// <span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
|
// <CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
|
// </Tag>,
|
||||||
|
// ]}
|
||||||
|
// extra={menuExtra}
|
||||||
|
// >
|
||||||
|
// <div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
// {(job.inproduction || jobInPostProduction) && (
|
||||||
|
// <>
|
||||||
|
// <div style={{ display: "flex", flex: 1 }}>
|
||||||
|
// <div style={{ marginRight: "2rem" }}>
|
||||||
|
// {t("jobs.fields.production_vars.note")}
|
||||||
|
// </div>
|
||||||
|
// <ProductionListColumnProductionNote record={job} />
|
||||||
|
// </div>
|
||||||
|
// <Divider type="vertical" />
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
//
|
||||||
|
// </div>
|
||||||
|
// </PageHeader>
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailHeader);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailHeader);
|
||||||
|
|||||||
@@ -15,14 +15,9 @@ export function JobsDetailRatesChangeButton({ disabled, form, bodyshop }) {
|
|||||||
|
|
||||||
const handleClick = ({ item, key, keyPath }) => {
|
const handleClick = ({ item, key, keyPath }) => {
|
||||||
const rate = item.props.value;
|
const rate = item.props.value;
|
||||||
console.log("handleClick -> rate", rate);
|
|
||||||
form.setFieldsValue(rate);
|
form.setFieldsValue(rate);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: jobs-detail-rates-change-button.component.jsx ~ line 26 ~ bodyshop.md_labor_rates",
|
|
||||||
bodyshop.md_labor_rates
|
|
||||||
);
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<div>
|
<div>
|
||||||
<Menu onClick={handleClick}>
|
<Menu onClick={handleClick}>
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsDetailRates({ job, jobRO, form }) {
|
export function JobsDetailRates({ jobRO, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
|
|||||||
@@ -1,143 +1,15 @@
|
|||||||
import { Button, Divider, Space, Statistic, Typography } from "antd";
|
import { Divider } from "antd";
|
||||||
import Dinero from "dinero.js";
|
import React from "react";
|
||||||
import React, { useMemo } from "react";
|
import JobPayments from "../job-payments/job-payments.component";
|
||||||
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 { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
import JobTotalsTable from "../job-totals-table/job-totals-table.component";
|
import JobTotalsTable from "../job-totals-table/job-totals-table.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
jobRO: selectJobReadOnly,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
setPaymentContext: (context) =>
|
|
||||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
|
||||||
});
|
|
||||||
|
|
||||||
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
|
|
||||||
|
|
||||||
export function JobsDetailTotals({
|
|
||||||
job,
|
|
||||||
jobRO,
|
|
||||||
bodyshop,
|
|
||||||
setPaymentContext,
|
|
||||||
refetch,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const total = useMemo(() => {
|
|
||||||
return (
|
|
||||||
job.payments &&
|
|
||||||
job.payments.reduce((acc, val) => {
|
|
||||||
acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) }));
|
|
||||||
return acc;
|
|
||||||
}, Dinero())
|
|
||||||
);
|
|
||||||
}, [job.payments]);
|
|
||||||
|
|
||||||
const balance = useMemo(() => {
|
|
||||||
if (job && job.job_totals && job.job_totals.totals.total_repairs)
|
|
||||||
return Dinero(job.job_totals.totals.total_repairs).subtract(total);
|
|
||||||
return Dinero({ amount: 0 }).subtract(total);
|
|
||||||
}, [job, total]);
|
|
||||||
|
|
||||||
|
export function JobsDetailTotals({ job, refetch }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4}>
|
<JobPayments job={job} refetch={refetch} />
|
||||||
{t("payments.labels.title")}
|
|
||||||
</Typography.Title>
|
|
||||||
|
|
||||||
<div className="imex-flex-row">
|
|
||||||
<table style={{ flex: 1 }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{t("payments.fields.created_at")}</th>
|
|
||||||
<th>{t("payments.fields.payer")}</th>
|
|
||||||
<th>{t("payments.fields.amount")}</th>
|
|
||||||
<th>{t("payments.fields.memo")}</th>
|
|
||||||
<th>{t("payments.fields.type")}</th>
|
|
||||||
<th>{t("payments.fields.transactionid")}</th>
|
|
||||||
<th>{t("payments.fields.stripeid")}</th>
|
|
||||||
<th>{t("general.labels.actions")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{job.payments.map((p, idx) => (
|
|
||||||
<tr key={idx}>
|
|
||||||
<td>
|
|
||||||
<DateTimeFormatter>{p.created_at}</DateTimeFormatter>
|
|
||||||
</td>
|
|
||||||
<td>{p.payer}</td>
|
|
||||||
<td>
|
|
||||||
<CurrencyFormatter>{p.amount}</CurrencyFormatter>
|
|
||||||
</td>
|
|
||||||
<td>{p.memo}</td>
|
|
||||||
<td>{p.type}</td>
|
|
||||||
<td>{p.transactionid}</td>
|
|
||||||
<td>
|
|
||||||
{p.stripeid ? (
|
|
||||||
<a
|
|
||||||
href={
|
|
||||||
stripeTestEnv
|
|
||||||
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${p.stripeid}`
|
|
||||||
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${p.stripeid}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{p.stripeid}
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<PrintWrapperComponent
|
|
||||||
templateObject={{
|
|
||||||
name: TemplateList("payment").payment_receipt.key,
|
|
||||||
variables: { id: p.id },
|
|
||||||
}}
|
|
||||||
messageObject={{
|
|
||||||
to: job.ownr_ea,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Button
|
|
||||||
disabled={jobRO}
|
|
||||||
onClick={() =>
|
|
||||||
setPaymentContext({
|
|
||||||
actions: { refetch: refetch },
|
|
||||||
context: { jobid: job.id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("menus.header.enterpayment")}
|
|
||||||
</Button>
|
|
||||||
<Statistic
|
|
||||||
title={t("payments.labels.totalpayments")}
|
|
||||||
value={total.toFormat()}
|
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title={t("payments.labels.balance")}
|
|
||||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
|
||||||
value={balance.toFormat()}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<JobTotalsTable job={job} />
|
<JobTotalsTable job={job} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailTotals);
|
export default JobsDetailTotals;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Button, Input, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -11,7 +11,6 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { onlyUnique } from "../../utils/arrayHelper";
|
import { onlyUnique } from "../../utils/arrayHelper";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||||
@@ -130,14 +129,10 @@ export function JobsList({ bodyshop }) {
|
|||||||
dataIndex: "ownr_ph1",
|
dataIndex: "ownr_ph1",
|
||||||
key: "ownr_ph1",
|
key: "ownr_ph1",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => {
|
responsive: ["md"],
|
||||||
return record.ownr_ph1 ? (
|
render: (text, record) => (
|
||||||
<span>
|
<StartChatButton phone={record.ownr_ph1} jobid={record.id} />
|
||||||
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
|
),
|
||||||
<StartChatButton phone={record.ownr_ph1} jobid={record.id} />
|
|
||||||
</span>
|
|
||||||
) : null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -160,9 +155,6 @@ export function JobsList({ bodyshop }) {
|
|||||||
})) ||
|
})) ||
|
||||||
[],
|
[],
|
||||||
onFilter: (value, record) => value.includes(record.status),
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
render: (text, record) => {
|
|
||||||
return record.status || t("general.labels.na");
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -188,28 +180,20 @@ export function JobsList({ bodyshop }) {
|
|||||||
title: t("vehicles.fields.plate_no"),
|
title: t("vehicles.fields.plate_no"),
|
||||||
dataIndex: "plate_no",
|
dataIndex: "plate_no",
|
||||||
key: "plate_no",
|
key: "plate_no",
|
||||||
|
responsive: ["md"],
|
||||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
|
||||||
return record.plate_no ? record.plate_no : "";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.clm_no"),
|
title: t("jobs.fields.clm_no"),
|
||||||
dataIndex: "clm_no",
|
dataIndex: "clm_no",
|
||||||
key: "clm_no",
|
key: "clm_no",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
|
||||||
return record.clm_no ? (
|
|
||||||
<span>{record.clm_no}</span>
|
|
||||||
) : (
|
|
||||||
t("general.labels.unknown")
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.ins_co_nm"),
|
title: t("jobs.fields.ins_co_nm"),
|
||||||
@@ -224,68 +208,65 @@ export function JobsList({ bodyshop }) {
|
|||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
|
||||||
return record.clm_total ? (
|
|
||||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
|
||||||
) : (
|
|
||||||
t("general.labels.unknown")
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.owner_owing"),
|
|
||||||
dataIndex: "owner_owing",
|
|
||||||
key: "owner_owing",
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
|
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// title: t("jobs.fields.owner_owing"),
|
||||||
|
// dataIndex: "owner_owing",
|
||||||
|
// key: "owner_owing",
|
||||||
|
// responsive: ["md"],
|
||||||
|
// render: (text, record) => (
|
||||||
|
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Card
|
||||||
loading={loading}
|
title={t("titles.bc.jobs-active")}
|
||||||
size="small"
|
extra={
|
||||||
pagination={false}
|
<Space wrap>
|
||||||
columns={columns}
|
<Button onClick={() => refetch()}>
|
||||||
rowKey="id"
|
<SyncOutlined />
|
||||||
dataSource={jobs}
|
</Button>
|
||||||
style={{ height: "100%" }}
|
<Input.Search
|
||||||
scroll={{ x: true }}
|
placeholder={t("general.labels.search")}
|
||||||
title={() => {
|
onChange={(e) => {
|
||||||
return (
|
setSearchText(e.target.value);
|
||||||
<div className="imex-table-header">
|
}}
|
||||||
<Button onClick={() => refetch()}>
|
value={searchText}
|
||||||
<SyncOutlined />
|
enterButton
|
||||||
</Button>
|
/>
|
||||||
<Input.Search
|
</Space>
|
||||||
className="imex-table-header__search"
|
}
|
||||||
placeholder={t("general.labels.search")}
|
>
|
||||||
onChange={(e) => {
|
<Table
|
||||||
setSearchText(e.target.value);
|
loading={loading}
|
||||||
}}
|
size="small"
|
||||||
value={searchText}
|
pagination={false}
|
||||||
enterButton
|
columns={columns}
|
||||||
/>
|
rowKey="id"
|
||||||
</div>
|
dataSource={jobs}
|
||||||
);
|
// scroll={{ x: true }}
|
||||||
}}
|
rowSelection={{
|
||||||
rowSelection={{
|
onSelect: (record) => {
|
||||||
onSelect: (record) => {
|
|
||||||
handleOnRowClick(record);
|
|
||||||
},
|
|
||||||
selectedRowKeys: [selected],
|
|
||||||
type: "radio",
|
|
||||||
}}
|
|
||||||
onChange={handleTableChange}
|
|
||||||
onRow={(record, rowIndex) => {
|
|
||||||
return {
|
|
||||||
onClick: (event) => {
|
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
},
|
},
|
||||||
};
|
selectedRowKeys: [selected],
|
||||||
}}
|
type: "radio",
|
||||||
/>
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ function RbacWrapper({
|
|||||||
authLevel ||
|
authLevel ||
|
||||||
(!bodyshop.md_rbac && rbacDefaults[action] <= authLevel)
|
(!bodyshop.md_rbac && rbacDefaults[action] <= authLevel)
|
||||||
)
|
)
|
||||||
return <div>{React.cloneElement(children, restProps)}</div>;
|
return children;
|
||||||
|
//return <div>{React.cloneElement(children, restProps)}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
noauth || (
|
noauth || (
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import AppContainer from "./App/App.container";
|
|||||||
import LoadingSpinner from "./components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "./components/loading-spinner/loading-spinner.component";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { persistor, store } from "./redux/store";
|
import { persistor, store } from "./redux/store";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
|
||||||
|
import reportWebVitals from "./reportWebVitals";
|
||||||
import "./translations/i18n";
|
import "./translations/i18n";
|
||||||
import "./utils/CleanAxios";
|
import "./utils/CleanAxios";
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
@@ -42,7 +43,7 @@ ReactDOM.render(
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<PersistGate
|
<PersistGate
|
||||||
loading={<LoadingSpinner message="PersistGate Loading." />}
|
loading={<LoadingSpinner message="Restoring your settings..." />}
|
||||||
persistor={persistor}
|
persistor={persistor}
|
||||||
>
|
>
|
||||||
<AppContainer />
|
<AppContainer />
|
||||||
@@ -54,7 +55,6 @@ ReactDOM.render(
|
|||||||
|
|
||||||
const onServiceWorkerUpdate = (registration) => {
|
const onServiceWorkerUpdate = (registration) => {
|
||||||
console.log("onServiceWorkerUpdate", registration);
|
console.log("onServiceWorkerUpdate", registration);
|
||||||
|
|
||||||
const key = `open${Date.now()}`;
|
const key = `open${Date.now()}`;
|
||||||
const btn = (
|
const btn = (
|
||||||
<Button
|
<Button
|
||||||
@@ -83,4 +83,5 @@ const onServiceWorkerUpdate = (registration) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
serviceWorker.register({ onUpdate: onServiceWorkerUpdate });
|
serviceWorkerRegistration.register({ onUpdate: onServiceWorkerUpdate });
|
||||||
|
reportWebVitals();
|
||||||
|
|||||||
@@ -3,140 +3,142 @@ import Icon, {
|
|||||||
CalendarFilled,
|
CalendarFilled,
|
||||||
DollarCircleOutlined,
|
DollarCircleOutlined,
|
||||||
FileImageFilled,
|
FileImageFilled,
|
||||||
|
PrinterFilled,
|
||||||
ToolFilled,
|
ToolFilled,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Form, notification, Tabs } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
notification,
|
||||||
|
PageHeader,
|
||||||
|
Space,
|
||||||
|
Tabs,
|
||||||
|
} from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import Dinero from "dinero.js";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaHardHat, FaRegStickyNote, FaShieldAlt } from "react-icons/fa";
|
import { FaHardHat, FaRegStickyNote, FaShieldAlt } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||||
//import JobsDetailChecklists from "../../components/jobs-detail-checklists/jobs-detail-checklists.component";
|
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
|
||||||
|
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||||
|
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||||
|
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component";
|
||||||
|
import JobsConvertButton from "../../components/jobs-convert-button/jobs-convert-button.component";
|
||||||
|
import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
|
||||||
|
import JobsDetailGeneral from "../../components/jobs-detail-general/jobs-detail-general.component";
|
||||||
|
import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component";
|
||||||
|
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component";
|
||||||
|
import JobsDetailLaborContainer from "../../components/jobs-detail-labor/jobs-detail-labor.container";
|
||||||
|
import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container";
|
||||||
|
import JobsDetailRates from "../../components/jobs-detail-rates/jobs-detail-rates.component";
|
||||||
|
import JobsDetailTotals from "../../components/jobs-detail-totals/jobs-detail-totals.component";
|
||||||
|
import JobsDocumentsGalleryContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
|
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
|
||||||
|
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const JobsLinesContainer = lazy(() =>
|
|
||||||
import("../../components/job-detail-lines/job-lines.container")
|
|
||||||
);
|
|
||||||
|
|
||||||
const JobsDetailDatesComponent = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-dates/jobs-detail-dates.component")
|
|
||||||
);
|
|
||||||
const JobsDetailTotals = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-totals/jobs-detail-totals.component")
|
|
||||||
);
|
|
||||||
const JobsDetailRates = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-rates/jobs-detail-rates.component")
|
|
||||||
);
|
|
||||||
const JobsDetailHeader = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-header/jobs-detail-header.component")
|
|
||||||
);
|
|
||||||
const JobsDetailGeneral = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-general/jobs-detail-general.component")
|
|
||||||
);
|
|
||||||
const JobsDocumentsGalleryContainer = lazy(() =>
|
|
||||||
import(
|
|
||||||
"../../components/jobs-documents-gallery/jobs-documents-gallery.container"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const JobNotesContainer = lazy(() =>
|
|
||||||
import("../../components/jobs-notes/jobs-notes.container")
|
|
||||||
);
|
|
||||||
const ScheduleJobModalContainer = lazy(() =>
|
|
||||||
import("../../components/schedule-job-modal/schedule-job-modal.container")
|
|
||||||
);
|
|
||||||
const JobLineUpsertModalContainer = lazy(() =>
|
|
||||||
import(
|
|
||||||
"../../components/job-lines-upsert-modal/job-lines-upsert-modal.container"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const JobsDetailPliContainer = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-pli/jobs-detail-pli.container")
|
|
||||||
);
|
|
||||||
// const JobsDetailAuditContainer = lazy(() =>
|
|
||||||
// import("../../components/audit-trail-list/audit-trail-list.container")
|
|
||||||
// );
|
|
||||||
const JobsDetailLaborContainer = lazy(() =>
|
|
||||||
import("../../components/jobs-detail-labor/jobs-detail-labor.container")
|
|
||||||
);
|
|
||||||
const JobReconciliationModal = lazy(() =>
|
|
||||||
import(
|
|
||||||
"../../components/job-reconciliation-modal/job-reconciliation.modal.container"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
|
});
|
||||||
export function JobsDetailPage({
|
export function JobsDetailPage({
|
||||||
|
setPrintCenterContext,
|
||||||
|
jobRO,
|
||||||
job,
|
job,
|
||||||
mutationUpdateJob,
|
mutationUpdateJob,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
refetch,
|
refetch,
|
||||||
jobRO,
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
const formItemLayout = {
|
const formItemLayout = {
|
||||||
layout: "vertical",
|
layout: "vertical",
|
||||||
// size: "small",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.resetFields();
|
//form.setFieldsValue(transormJobToForm(job));
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}, [form, job]);
|
}, [form, job]);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
//const newTotals = CalculateJob({ ...job, ...values }, bodyshop.shoprates);
|
|
||||||
const newTotals = (
|
|
||||||
await Axios.post("/job/totals", {
|
|
||||||
job: { ...job, ...values },
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
const result = await mutationUpdateJob({
|
const result = await mutationUpdateJob({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: {
|
job: values,
|
||||||
...values,
|
|
||||||
clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"),
|
|
||||||
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat(
|
|
||||||
"0.00"
|
|
||||||
),
|
|
||||||
job_totals: newTotals, //JSON.stringify(newTotals),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const newTotals = await Axios.post("/job/totalsssu", {
|
||||||
|
id: job.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (newTotals.status !== 200 || result.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.totalscalc"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("jobs.successes.savetitle"),
|
message: t("jobs.successes.savetitle"),
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
form.resetFields();
|
form.setFieldsValue(transormJobToForm(job));
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const menuExtra = (
|
||||||
|
<Space wrap>
|
||||||
|
<JobsChangeStatus job={job} />
|
||||||
|
<JobSyncButton job={job} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPrintCenterContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
id: job.id,
|
||||||
|
job: job,
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
key="printing"
|
||||||
|
>
|
||||||
|
<PrinterFilled />
|
||||||
|
{t("jobs.actions.printCenter")}
|
||||||
|
</Button>
|
||||||
|
<JobsConvertButton job={job} refetch={refetch} />
|
||||||
|
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
disabled={jobRO}
|
||||||
|
onClick={() => form.submit()}
|
||||||
|
>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<div>
|
||||||
fallback={<LoadingSpinner message={t("general.labels.loadingapp")} />}
|
|
||||||
>
|
|
||||||
<ScheduleJobModalContainer />
|
<ScheduleJobModalContainer />
|
||||||
<JobReconciliationModal />
|
<JobReconciliationModal />
|
||||||
<JobLineUpsertModalContainer />
|
<JobLineUpsertModalContainer />
|
||||||
@@ -146,28 +148,23 @@ export function JobsDetailPage({
|
|||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
autoComplete={"off"}
|
autoComplete={"off"}
|
||||||
initialValues={{
|
initialValues={transormJobToForm(job)}
|
||||||
...job,
|
|
||||||
loss_date: job.loss_date ? moment(job.loss_date) : null,
|
|
||||||
date_estimated: job.date_estimated
|
|
||||||
? moment(job.date_estimated)
|
|
||||||
: null,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FormFieldsChanged form={form} />
|
<PageHeader
|
||||||
|
onBack={() => window.history.back()}
|
||||||
<JobsDetailHeader
|
title={job.ro_number || t("general.labels.na")}
|
||||||
form={form}
|
extra={menuExtra}
|
||||||
job={job}
|
|
||||||
refetch={refetch}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
/>
|
||||||
|
<JobsDetailHeader job={job} />
|
||||||
|
<Divider type="horizontal" />
|
||||||
|
<FormFieldsChanged form={form} />
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey={search.tab}
|
defaultActiveKey={search.tab}
|
||||||
onChange={(key) => history.push({ search: `?tab=${key}` })}
|
onChange={(key) => history.push({ search: `?tab=${key}` })}
|
||||||
|
tabBarStyle={{ fontWeight: "bold", borderBottom: "10px" }}
|
||||||
>
|
>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
forceRender
|
||||||
tab={
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<Icon component={FaShieldAlt} />
|
<Icon component={FaShieldAlt} />
|
||||||
@@ -179,6 +176,7 @@ export function JobsDetailPage({
|
|||||||
<JobsDetailGeneral job={job} form={form} />
|
<JobsDetailGeneral job={job} form={form} />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
forceRender
|
||||||
tab={
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<BarsOutlined />
|
<BarsOutlined />
|
||||||
@@ -195,6 +193,7 @@ export function JobsDetailPage({
|
|||||||
/>
|
/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
forceRender
|
||||||
tab={
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<DollarCircleOutlined />
|
<DollarCircleOutlined />
|
||||||
@@ -239,6 +238,7 @@ export function JobsDetailPage({
|
|||||||
<JobsDetailLaborContainer jobId={job.id} />
|
<JobsDetailLaborContainer jobId={job.id} />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
forceRender
|
||||||
tab={
|
tab={
|
||||||
<span>
|
<span>
|
||||||
<CalendarFilled />
|
<CalendarFilled />
|
||||||
@@ -271,33 +271,17 @@ export function JobsDetailPage({
|
|||||||
>
|
>
|
||||||
<JobNotesContainer jobId={job.id} />
|
<JobNotesContainer jobId={job.id} />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
{
|
|
||||||
// <Tabs.TabPane
|
|
||||||
// tab={
|
|
||||||
// <span>
|
|
||||||
// <Icon component={FaHistory} />
|
|
||||||
// {t("jobs.labels.audit")}
|
|
||||||
// </span>
|
|
||||||
// }
|
|
||||||
// key="audit"
|
|
||||||
// >
|
|
||||||
// <JobsDetailAuditContainer recordId={job.id} />
|
|
||||||
// </Tabs.TabPane>
|
|
||||||
// <Tabs.TabPane
|
|
||||||
// tab={
|
|
||||||
// <span>
|
|
||||||
// <CheckSquareFilled />
|
|
||||||
// {t("jobs.labels.checklists")}
|
|
||||||
// </span>
|
|
||||||
// }
|
|
||||||
// key="checklists"
|
|
||||||
// >
|
|
||||||
// <JobsDetailChecklists job={job} />
|
|
||||||
// </Tabs.TabPane>
|
|
||||||
}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Form>
|
</Form>
|
||||||
</Suspense>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(JobsDetailPage);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailPage);
|
||||||
|
|
||||||
|
const transormJobToForm = (job) => {
|
||||||
|
return {
|
||||||
|
...job,
|
||||||
|
loss_date: job.loss_date ? moment(job.loss_date) : null,
|
||||||
|
date_estimated: job.date_estimated ? moment(job.date_estimated) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import JobDetailCards from "../../components/job-detail-cards/job-detail-cards.component";
|
import JobDetailCards from "../../components/job-detail-cards/job-detail-cards.component";
|
||||||
import JobsList from "../../components/jobs-list/jobs-list.component";
|
import JobsList from "../../components/jobs-list/jobs-list.component";
|
||||||
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import {
|
import {
|
||||||
setBreadcrumbs,
|
setBreadcrumbs,
|
||||||
setSelectedHeader,
|
setSelectedHeader,
|
||||||
} from "../../redux/application/application.actions";
|
} from "../../redux/application/application.actions";
|
||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
@@ -27,10 +27,8 @@ export function JobsPage({ setBreadcrumbs, setSelectedHeader }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="jobs:list-active">
|
<RbacWrapper action="jobs:list-active">
|
||||||
<div className="jobs-list-container">
|
<JobsList />
|
||||||
<JobsList />
|
<JobDetailCards />
|
||||||
<JobDetailCards />
|
|
||||||
</div>
|
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ import ChatAffixContainer from "../../components/chat-affix/chat-affix.container
|
|||||||
import ConflictComponent from "../../components/conflict/conflict.component";
|
import ConflictComponent from "../../components/conflict/conflict.component";
|
||||||
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
|
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
|
||||||
import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
|
import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
|
||||||
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
|
||||||
//import FooterComponent from "../../components/footer/footer.component";
|
//import FooterComponent from "../../components/footer/footer.component";
|
||||||
//Component Imports
|
//Component Imports
|
||||||
|
|
||||||
import HeaderContainer from "../../components/header/header.container";
|
import HeaderContainer from "../../components/header/header.container";
|
||||||
import JiraSupportComponent from "../../components/jira-support-widget/jira-support-widget.component";
|
import JiraSupportComponent from "../../components/jira-support-widget/jira-support-widget.component";
|
||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||||
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
|
||||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||||
|
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
|
||||||
import TestComponent from "../../components/_test/test.component";
|
import TestComponent from "../../components/_test/test.component";
|
||||||
import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries";
|
import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries";
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +33,7 @@ const ManageRootPage = lazy(() =>
|
|||||||
import("../manage-root/manage-root.page.container")
|
import("../manage-root/manage-root.page.container")
|
||||||
);
|
);
|
||||||
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||||
|
|
||||||
const JobsDetailPage = lazy(() =>
|
const JobsDetailPage = lazy(() =>
|
||||||
import("../jobs-detail/jobs-detail.page.container")
|
import("../jobs-detail/jobs-detail.page.container")
|
||||||
);
|
);
|
||||||
@@ -154,7 +154,7 @@ const EmailTest = lazy(() =>
|
|||||||
import("../../components/email-test/email-test-component")
|
import("../../components/email-test/email-test-component")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { Content, Header } = Layout;
|
const { Content, Footer } = Layout;
|
||||||
|
|
||||||
const stripePromise = new Promise((resolve, reject) => {
|
const stripePromise = new Promise((resolve, reject) => {
|
||||||
client.query({ query: QUERY_STRIPE_ID }).then((resp) => {
|
client.query({ query: QUERY_STRIPE_ID }).then((resp) => {
|
||||||
@@ -197,7 +197,6 @@ export function Manage({ match, conflict, bodyshop }) {
|
|||||||
<PrintCenterModalContainer />
|
<PrintCenterModalContainer />
|
||||||
<Route exact path={`${match.path}/_test`} component={TestComponent} />
|
<Route exact path={`${match.path}/_test`} component={TestComponent} />
|
||||||
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
||||||
|
|
||||||
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
@@ -365,27 +364,27 @@ export function Manage({ match, conflict, bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="layout-container">
|
<Layout className="layout-container">
|
||||||
<Header>
|
<HeaderContainer />
|
||||||
<HeaderContainer />
|
|
||||||
</Header>
|
|
||||||
<Content className="content-container">
|
<Content className="content-container">
|
||||||
<FcmNotification />
|
<FcmNotification />
|
||||||
<PartnerPingComponent />
|
<PartnerPingComponent />
|
||||||
<ErrorBoundary>{PageContent}</ErrorBoundary>
|
<ErrorBoundary>{PageContent}</ErrorBoundary>
|
||||||
<ChatAffixContainer />
|
<ChatAffixContainer />
|
||||||
|
|
||||||
<BackTop />
|
<BackTop />
|
||||||
<div style={{ textAlign: "center", margin: "1rem 0rem" }}>
|
<Footer>
|
||||||
<div>
|
<div style={{ textAlign: "center", margin: "1rem 0rem" }}>
|
||||||
{`ImEX Online V.${process.env.NODE_ENV} - ${
|
<div>
|
||||||
process.env.REACT_APP_GIT_SHA
|
{`ImEX Online V.${process.env.NODE_ENV} - ${
|
||||||
} - ${preval`module.exports = new Date().toLocaleString();`}`}
|
process.env.REACT_APP_GIT_SHA
|
||||||
|
} - ${preval`module.exports = new Date().toLocaleString();`}`}
|
||||||
|
</div>
|
||||||
|
<Link to="/about" target="_blank" style={{ color: "#ccc" }}>
|
||||||
|
Disclaimer
|
||||||
|
</Link>
|
||||||
|
<JiraSupportComponent />
|
||||||
</div>
|
</div>
|
||||||
<Link to="/about" target="_blank" style={{ color: "#ccc" }}>
|
</Footer>
|
||||||
Disclaimer
|
|
||||||
</Link>
|
|
||||||
<JiraSupportComponent />
|
|
||||||
</div>
|
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ import { useQuery } from "@apollo/client";
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import AlertComponent from "../../components/alert/alert.component";
|
import AlertComponent from "../../components/alert/alert.component";
|
||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||||
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
|
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
|
||||||
import { setBodyshop } from "../../redux/user/user.actions";
|
import { setBodyshop } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import ManagePage from "./manage.page.component";
|
import ManagePage from "./manage.page.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBodyshop: (bs) => dispatch(setBodyshop(bs)),
|
setBodyshop: (bs) => dispatch(setBodyshop(bs)),
|
||||||
});
|
});
|
||||||
|
|
||||||
function ManagePageContainer({ match, setBodyshop, bodyshop }) {
|
function ManagePageContainer({ match, setBodyshop }) {
|
||||||
const { loading, error, data } = useQuery(QUERY_BODYSHOP, {
|
const { loading, error, data } = useQuery(QUERY_BODYSHOP, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
@@ -34,7 +30,4 @@ function ManagePageContainer({ match, setBodyshop, bodyshop }) {
|
|||||||
return <ManagePage match={match} />;
|
return <ManagePage match={match} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(null, mapDispatchToProps)(ManagePageContainer);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ManagePageContainer);
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
.content-container {
|
.content-container {
|
||||||
overflow-y: auto;
|
padding: 1rem;
|
||||||
margin: 1rem 1rem 0rem 1rem;
|
|
||||||
padding: 0.25rem 2.5rem 1rem 2.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-container {
|
.layout-container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
13
client/src/reportWebVitals.js
Normal file
13
client/src/reportWebVitals.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = (onPerfEntry) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
72
client/src/service-worker.js
Normal file
72
client/src/service-worker.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
|
// This service worker can be customized!
|
||||||
|
// See https://developers.google.com/web/tools/workbox/modules
|
||||||
|
// for the list of available Workbox modules, or add any other
|
||||||
|
// code you'd like.
|
||||||
|
// You can also remove this file if you'd prefer not to use a
|
||||||
|
// service worker, and the Workbox build step will be skipped.
|
||||||
|
|
||||||
|
import { clientsClaim } from 'workbox-core';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { StaleWhileRevalidate } from 'workbox-strategies';
|
||||||
|
|
||||||
|
clientsClaim();
|
||||||
|
|
||||||
|
// Precache all of the assets generated by your build process.
|
||||||
|
// Their URLs are injected into the manifest variable below.
|
||||||
|
// This variable must be present somewhere in your service worker file,
|
||||||
|
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// Set up App Shell-style routing, so that all navigation requests
|
||||||
|
// are fulfilled with your index.html shell. Learn more at
|
||||||
|
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||||
|
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
||||||
|
registerRoute(
|
||||||
|
// Return false to exempt requests from being fulfilled by index.html.
|
||||||
|
({ request, url }) => {
|
||||||
|
// If this isn't a navigation, skip.
|
||||||
|
if (request.mode !== 'navigate') {
|
||||||
|
return false;
|
||||||
|
} // If this is a URL that starts with /_, skip.
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/_')) {
|
||||||
|
return false;
|
||||||
|
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||||
|
|
||||||
|
if (url.pathname.match(fileExtensionRegexp)) {
|
||||||
|
return false;
|
||||||
|
} // Return true to signal that we want to use the handler.
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||||
|
);
|
||||||
|
|
||||||
|
// An example runtime caching route for requests that aren't handled by the
|
||||||
|
// precache, in this case same-origin .png requests like those from in public/
|
||||||
|
registerRoute(
|
||||||
|
// Add in any other file extensions or routing criteria as needed.
|
||||||
|
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'images',
|
||||||
|
plugins: [
|
||||||
|
// Ensure that once this runtime cache reaches a maximum size the
|
||||||
|
// least-recently used images are removed.
|
||||||
|
new ExpirationPlugin({ maxEntries: 50 }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// This allows the web app to trigger skipWaiting via
|
||||||
|
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any other custom service worker logic can go here.
|
||||||
137
client/src/serviceWorkerRegistration.js
Normal file
137
client/src/serviceWorkerRegistration.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://cra.link/PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://cra.link/PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then((registration) => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('No internet connection found. App is running in offline mode.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1001,6 +1001,7 @@
|
|||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "Insurance Co. ID",
|
"ins_co_id": "Insurance Co. ID",
|
||||||
"ins_co_nm": "Insurance Company Name",
|
"ins_co_nm": "Insurance Company Name",
|
||||||
|
"ins_co_nm_short": "Ins. Co.",
|
||||||
"ins_ct_fn": "File Handler First Name",
|
"ins_ct_fn": "File Handler First Name",
|
||||||
"ins_ct_ln": "File Handler Last Name",
|
"ins_ct_ln": "File Handler Last Name",
|
||||||
"ins_ea": "File Handler Email",
|
"ins_ea": "File Handler Email",
|
||||||
@@ -1068,7 +1069,7 @@
|
|||||||
"policy_no": "Policy #",
|
"policy_no": "Policy #",
|
||||||
"ponumber": "PO Number",
|
"ponumber": "PO Number",
|
||||||
"production_vars": {
|
"production_vars": {
|
||||||
"note": "Production Note:"
|
"note": "Production Note"
|
||||||
},
|
},
|
||||||
"rate_la1": "LA1",
|
"rate_la1": "LA1",
|
||||||
"rate_la2": "LA2",
|
"rate_la2": "LA2",
|
||||||
@@ -1191,6 +1192,7 @@
|
|||||||
"documents-other": "Other Documents",
|
"documents-other": "Other Documents",
|
||||||
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
|
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
|
||||||
"employeeassignments": "Employee Assignments",
|
"employeeassignments": "Employee Assignments",
|
||||||
|
"estimatelines": "Estimate Lines",
|
||||||
"existing_jobs": "Existing Jobs",
|
"existing_jobs": "Existing Jobs",
|
||||||
"federal_tax_amt": "Federal Taxes",
|
"federal_tax_amt": "Federal Taxes",
|
||||||
"gpdollars": "$ G.P.",
|
"gpdollars": "$ G.P.",
|
||||||
|
|||||||
@@ -1001,6 +1001,7 @@
|
|||||||
"ins_city": "Ciudad de seguros",
|
"ins_city": "Ciudad de seguros",
|
||||||
"ins_co_id": "ID de la compañía de seguros",
|
"ins_co_id": "ID de la compañía de seguros",
|
||||||
"ins_co_nm": "Nombre de la compañía de seguros",
|
"ins_co_nm": "Nombre de la compañía de seguros",
|
||||||
|
"ins_co_nm_short": "",
|
||||||
"ins_ct_fn": "Nombre del controlador de archivos",
|
"ins_ct_fn": "Nombre del controlador de archivos",
|
||||||
"ins_ct_ln": "Apellido del manejador de archivos",
|
"ins_ct_ln": "Apellido del manejador de archivos",
|
||||||
"ins_ea": "Correo electrónico del controlador de archivos",
|
"ins_ea": "Correo electrónico del controlador de archivos",
|
||||||
@@ -1191,6 +1192,7 @@
|
|||||||
"documents-other": "",
|
"documents-other": "",
|
||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"estimatelines": "",
|
||||||
"existing_jobs": "Empleos existentes",
|
"existing_jobs": "Empleos existentes",
|
||||||
"federal_tax_amt": "",
|
"federal_tax_amt": "",
|
||||||
"gpdollars": "",
|
"gpdollars": "",
|
||||||
|
|||||||
@@ -1001,6 +1001,7 @@
|
|||||||
"ins_city": "Insurance City",
|
"ins_city": "Insurance City",
|
||||||
"ins_co_id": "ID de la compagnie d'assurance",
|
"ins_co_id": "ID de la compagnie d'assurance",
|
||||||
"ins_co_nm": "Nom de la compagnie d'assurance",
|
"ins_co_nm": "Nom de la compagnie d'assurance",
|
||||||
|
"ins_co_nm_short": "",
|
||||||
"ins_ct_fn": "Prénom du gestionnaire de fichiers",
|
"ins_ct_fn": "Prénom du gestionnaire de fichiers",
|
||||||
"ins_ct_ln": "Nom du gestionnaire de fichiers",
|
"ins_ct_ln": "Nom du gestionnaire de fichiers",
|
||||||
"ins_ea": "Courriel du gestionnaire de fichiers",
|
"ins_ea": "Courriel du gestionnaire de fichiers",
|
||||||
@@ -1191,6 +1192,7 @@
|
|||||||
"documents-other": "",
|
"documents-other": "",
|
||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"estimatelines": "",
|
||||||
"existing_jobs": "Emplois existants",
|
"existing_jobs": "Emplois existants",
|
||||||
"federal_tax_amt": "",
|
"federal_tax_amt": "",
|
||||||
"gpdollars": "",
|
"gpdollars": "",
|
||||||
|
|||||||
18
client/src/utils/usetraceupdate.jsx
Normal file
18
client/src/utils/usetraceupdate.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
function useTraceUpdate(props) {
|
||||||
|
const prev = useRef(props);
|
||||||
|
useEffect(() => {
|
||||||
|
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
||||||
|
if (prev.current[k] !== v) {
|
||||||
|
ps[k] = [prev.current[k], v];
|
||||||
|
}
|
||||||
|
return ps;
|
||||||
|
}, {});
|
||||||
|
if (Object.keys(changedProps).length > 0) {
|
||||||
|
console.log("Changed props:", changedProps);
|
||||||
|
}
|
||||||
|
prev.current = props;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTraceUpdate;
|
||||||
14000
client/yarn.lock
Normal file
14000
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user