Compare commits
1 Commits
feature/20
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf75ad68f9 |
@@ -1 +1 @@
|
||||
client_max_body_size 50M;
|
||||
client_max_body_size 15M;
|
||||
@@ -22,7 +22,7 @@ hooks.js:
|
||||
module.exports = [
|
||||
{
|
||||
path: "/pull",
|
||||
command: "git pull && yarn && pm2 restart 0",
|
||||
command: "git pull && npm i",
|
||||
cwd: "/home/ubuntu/io/",
|
||||
method: "post",
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ import JobsShow from "../jobs/jobs.show";
|
||||
const httpLink = new HttpLink({
|
||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||
headers: {
|
||||
"x-hasura-admin-secret": `Dev-BodyShopApp!`,
|
||||
"x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
|
||||
// 'Authorization': `Bearer xxxx`,
|
||||
},
|
||||
});
|
||||
@@ -67,7 +67,7 @@ const client = new ApolloClient({
|
||||
// uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||
// cache: new InMemoryCache(),
|
||||
// headers: {
|
||||
// "x-hasura-admin-secret": `Dev-BodyShopApp!`,
|
||||
// "x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
|
||||
// // 'Authorization': `Bearer xxxx`,
|
||||
// },
|
||||
// });
|
||||
|
||||
@@ -12,9 +12,7 @@ module.exports = {
|
||||
modifyVars: {
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { "@primary-color": "#a51d1d" }
|
||||
: {
|
||||
//"@primary-color": "#1DA57A"
|
||||
}),
|
||||
: { "@primary-color": "#1DA57A" }),
|
||||
// "@primary-color": " #1890ff", // primary color for all components
|
||||
// "@link-color": "#1890ff", // link color
|
||||
// "@success-color": "#52c41a", // success state color
|
||||
|
||||
13
client/madge-graph.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.44.1 (20200629.0846)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="43pt" height="43pt"
|
||||
viewBox="0.00 0.00 43.20 43.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(21.6 21.6)">
|
||||
<title>G</title>
|
||||
<polygon fill="#111111" stroke="transparent" points="-21.6,21.6 -21.6,-21.6 21.6,-21.6 21.6,21.6 -21.6,21.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 613 B |
42958
client/package-lock.json
generated
Normal file
@@ -5,7 +5,7 @@
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.17",
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@craco/craco": "^6.1.2",
|
||||
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
||||
"@lourenci/react-kanban": "^2.1.0",
|
||||
"@sentry/react": "^6.3.6",
|
||||
@@ -19,9 +19,7 @@
|
||||
"craco-less": "^1.17.1",
|
||||
"dinero.js": "^1.8.1",
|
||||
"dotenv": "^9.0.2",
|
||||
"enquire-js": "^0.2.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.0.0",
|
||||
"firebase": "^8.6.0",
|
||||
"graphql": "^15.5.0",
|
||||
"i18next": "^20.2.2",
|
||||
@@ -30,21 +28,17 @@
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.9.17",
|
||||
"logrocket": "^1.2.0",
|
||||
"markerjs2": "^2.8.1",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"phone": "^2.4.21",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^7.0.0",
|
||||
"rc-queue-anim": "^1.8.5",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^17.0.1",
|
||||
"react-big-calendar": "^0.33.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-drag-listview": "^0.1.8",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^1.2.5",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-number-format": "^4.5.5",
|
||||
@@ -52,7 +46,6 @@
|
||||
"react-resizable": "^3.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.0.7",
|
||||
"redux": "^4.1.0",
|
||||
@@ -61,7 +54,6 @@
|
||||
"redux-state-sync": "^3.1.2",
|
||||
"reselect": "^4.0.0",
|
||||
"sass": "^1.32.13",
|
||||
"socket.io-client": "^4.1.2",
|
||||
"styled-components": "^5.3.0",
|
||||
"subscriptions-transport-ws": "^0.9.18",
|
||||
"web-vitals": "^1.1.2",
|
||||
@@ -82,8 +74,8 @@
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "craco start",
|
||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||
"build:test": "env-cmd -f .env.test yarn run build",
|
||||
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||
"build:test": "env-cmd -f .env.test npm run build",
|
||||
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
@@ -8,61 +8,13 @@
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement("script");
|
||||
s.src = "https://client.crisp.chat/l.js";
|
||||
s.async = 1;
|
||||
d.getElementsByTagName("head")[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
!(function () {
|
||||
"use strict";
|
||||
var e = [
|
||||
"debug",
|
||||
"destroy",
|
||||
"do",
|
||||
"help",
|
||||
"identify",
|
||||
"is",
|
||||
"off",
|
||||
"on",
|
||||
"ready",
|
||||
"render",
|
||||
"reset",
|
||||
"safe",
|
||||
"set",
|
||||
];
|
||||
if (window.noticeable)
|
||||
console.warn("Noticeable SDK code snippet loaded more than once");
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement("script");
|
||||
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- <script
|
||||
data-jsd-embedded
|
||||
data-key="51adb36e-ee16-46b1-a4c6-4b6d5fcd8530"
|
||||
data-base-url="https://jsd-widget.atlassian.com"
|
||||
src="https://jsd-widget.atlassian.com/assets/embed.js"
|
||||
></script> -->
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
|
||||
@@ -4,17 +4,16 @@ import enLocale from "antd/es/locale/en_US";
|
||||
import LogRocket from "logrocket";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import { useTranslation } from "react-i18next";
|
||||
moment.locale("en-US");
|
||||
|
||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||
|
||||
export default function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Button, Result } from "antd";
|
||||
import React, { lazy, Suspense, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||
//Component Imports
|
||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||
import AboutPage from "../pages/about/about.page";
|
||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
||||
import { setOnline } from "../redux/application/application.actions";
|
||||
import { selectOnline } from "../redux/application/application.selectors";
|
||||
import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||
import PrivateRoute from "../utils/private-route";
|
||||
import "./App.styles.scss";
|
||||
|
||||
import LandingPage from "../pages/landing/landing.page";
|
||||
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
|
||||
const ResetPassword = lazy(() =>
|
||||
import("../pages/reset-password/reset-password.component")
|
||||
);
|
||||
@@ -31,59 +27,25 @@ const MobilePaymentContainer = lazy(() =>
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
online: selectOnline,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkUserSession: () => dispatch(checkUserSession()),
|
||||
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
||||
});
|
||||
|
||||
export function App({ checkUserSession, currentUser, online, setOnline }) {
|
||||
export function App({ checkUserSession, currentUser }) {
|
||||
useEffect(() => {
|
||||
if (!navigator.onLine) {
|
||||
setOnline(false);
|
||||
}
|
||||
|
||||
checkUserSession();
|
||||
}, [checkUserSession, setOnline]);
|
||||
}, [checkUserSession]);
|
||||
|
||||
//const b = Grid.useBreakpoint();
|
||||
// console.log("Breakpoints:", b);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
window.addEventListener("offline", function (e) {
|
||||
console.log("Internet connection lost.");
|
||||
setOnline(false);
|
||||
});
|
||||
|
||||
window.addEventListener("online", function (e) {
|
||||
setOnline(true);
|
||||
});
|
||||
|
||||
if (currentUser.authorized === null) {
|
||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||
}
|
||||
|
||||
if (!online)
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.nointernet")}
|
||||
subTitle={t("general.labels.nointernet_sub")}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
||||
@@ -100,7 +62,7 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
|
||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
||||
<Route exact path="/about" component={AboutPage} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Route
|
||||
@@ -123,13 +85,6 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
|
||||
component={TechPageContainer}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute
|
||||
isAuthorized={currentUser.authorized}
|
||||
path="/edit"
|
||||
component={DocumentEditorContainer}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -118,9 +118,3 @@
|
||||
.production-list-min-height {
|
||||
min-height: 19px;
|
||||
}
|
||||
|
||||
#noticeable-widget {
|
||||
iframe {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 177 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" ?><svg style="enable-background:new 0 0 128 128;" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:none;stroke:#000000;stroke-width:8;stroke-miterlimit:10;}
|
||||
.st1{display:none;}
|
||||
.st2{display:inline;opacity:0.25;fill:#F45EFD;}
|
||||
</style><g id="_x31_2_3D_Printing"/><g id="_x31_1_VR_Gear"/><g id="_x31_0_Virtual_reality"/><g id="_x39__Augmented_reality"/><g id="_x38__Teleport"/><g id="_x37__Glassess"/><g id="_x36__Folding_phone"/><g id="_x35__Drone"/><g id="_x34__Retina_scan"/><g id="_x33__Smartwatch"/><g id="_x32__Bionic_Arm"/><g id="_x31__Chip"><g><path d="M108,40c-5.2,0-9.6,3.3-11.3,8H84V32h-8V20h-8v12h-8V20h-8v12h-8v16H24v-8.7c4.7-1.7,8-6.1,8-11.3c0-6.6-5.4-12-12-12 S8,21.4,8,28c0,5.2,3.3,9.6,8,11.3V56h28v8H16v16.7c-4.7,1.7-8,6.1-8,11.3c0,6.6,5.4,12,12,12s12-5.4,12-12c0-5.2-3.3-9.6-8-11.3 V72h20v16h8v12h8V88h8v12h8V88h8V72h8v16.7c-4.7,1.7-8,6.1-8,11.3c0,6.6,5.4,12,12,12s12-5.4,12-12c0-5.2-3.3-9.6-8-11.3V64H84v-8 h12.7c1.7,4.7,6.1,8,11.3,8c6.6,0,12-5.4,12-12S114.6,40,108,40z M20,32c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S22.2,32,20,32z M20,96c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S22.2,96,20,96z M76,80H52V40h24V80z M96,96c2.2,0,4,1.8,4,4s-1.8,4-4,4s-4-1.8-4-4 S93.8,96,96,96z M108,56c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S110.2,56,108,56z"/><rect height="8" width="8" x="56" y="64"/></g></g><g class="st1" id="Guide"><path class="st2" d="M120,8v112H8V8H120 M128,0H0v128h128V0L128,0z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
|
||||
|
||||
export default function AccountingPayablesTableComponent({
|
||||
loading,
|
||||
@@ -41,12 +41,19 @@ export default function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
sorter: (a, b) => alphaSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => alphaSort(a.date, b.date),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
@@ -54,7 +61,7 @@ export default function AccountingPayablesTableComponent({
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
state.sortedInfo.columnKey === "ownr_ln" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
|
||||
@@ -81,7 +81,6 @@ function BillEnterModalContainer({
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||
});
|
||||
console.log("adjustmentsToInsert", adjustmentsToInsert);
|
||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||
|
||||
@@ -41,7 +41,6 @@ export function BillFormComponent({
|
||||
loadLines,
|
||||
billEdit,
|
||||
disableInvNumber,
|
||||
job,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -51,10 +50,6 @@ export function BillFormComponent({
|
||||
setDiscount(opt.discount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (job) form.validateFields(["is_credit_memo"]);
|
||||
}, [job, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
||||
const vendorId = form.getFieldValue("vendorid");
|
||||
@@ -94,7 +89,7 @@ export function BillFormComponent({
|
||||
<JobSearchSelect
|
||||
disabled={billEdit || disabled}
|
||||
convertedOnly
|
||||
notExported={false}
|
||||
// notExported={false}
|
||||
onBlur={() => {
|
||||
if (form.getFieldValue("jobid") !== null) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
@@ -111,18 +106,6 @@ export function BillFormComponent({
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (
|
||||
value &&
|
||||
!getFieldValue(["isinhouse"]) &&
|
||||
value === bodyshop.inhousevendorid
|
||||
) {
|
||||
return Promise.reject(t("bills.validation.manualinhouse"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
@@ -192,22 +175,6 @@ export function BillFormComponent({
|
||||
label={t("bills.fields.is_credit_memo")}
|
||||
name="is_credit_memo"
|
||||
valuePropName="checked"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (
|
||||
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
||||
job.status === bodyshop.md_ro_statuses.default_exported ||
|
||||
job.status === bodyshop.md_ro_statuses.default_void) &&
|
||||
(value === false || !value)
|
||||
) {
|
||||
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
@@ -34,7 +34,6 @@ export function BillFormContainer({
|
||||
}
|
||||
loadLines={loadLines}
|
||||
lineData={lineData ? lineData.joblines : []}
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
/>
|
||||
|
||||
@@ -47,9 +47,7 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("bills.successes.reexport"),
|
||||
});
|
||||
notification["success"]({ message: t("bills.successes.save") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bills.errors.saving", {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -47,7 +47,7 @@ export function BillsListTableComponent({
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<EditFilled />
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} />
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { HomeFilled } from "@ant-design/icons";
|
||||
import { Breadcrumb, Row, Col } from "antd";
|
||||
import { Breadcrumb } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -14,29 +13,24 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function BreadCrumbs({ breadcrumbs }) {
|
||||
return (
|
||||
<Row className="breadcrumb-container">
|
||||
<Col xs={24} sm={24} md={16}>
|
||||
<Breadcrumb separator=">">
|
||||
<Breadcrumb.Item>
|
||||
<Link to={`/manage`}>
|
||||
<HomeFilled />
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
{breadcrumbs.map((item) =>
|
||||
item.link ? (
|
||||
<Breadcrumb.Item key={item.label}>
|
||||
<Link to={item.link}>{item.label} </Link>
|
||||
</Breadcrumb.Item>
|
||||
) : (
|
||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
||||
)
|
||||
)}
|
||||
</Breadcrumb>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<GlobalSearch />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="breadcrumb-container imex-flex-row">
|
||||
<Breadcrumb separator=">">
|
||||
<Breadcrumb.Item>
|
||||
<Link to={`/manage`}>
|
||||
<HomeFilled />
|
||||
</Link>
|
||||
</Breadcrumb.Item>
|
||||
{breadcrumbs.map((item) =>
|
||||
item.link ? (
|
||||
<Breadcrumb.Item key={item.label}>
|
||||
<Link to={item.link}>{item.label} </Link>
|
||||
</Breadcrumb.Item>
|
||||
) : (
|
||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
||||
)
|
||||
)}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useSubscription } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ChatAffixComponent from "./chat-affix.component";
|
||||
import { Affix } from "antd";
|
||||
import "./chat-affix.styles.scss";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
chatVisible: selectChatVisible,
|
||||
@@ -29,20 +31,22 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
return (
|
||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
<div>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
conversationList={(data && data.conversations) || []}
|
||||
unreadCount={
|
||||
(data &&
|
||||
data.conversations.reduce((acc, val) => {
|
||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||
}, 0)) ||
|
||||
0
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
.chat-affix {
|
||||
position: fixed;
|
||||
left: 2vw;
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
z-index: 999;
|
||||
-webkit-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
-moz-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
|
||||
}
|
||||
|
||||
.chat-affix-open {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||
|
||||
export default function ChatArchiveButton({ conversation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||
const handleToggleArchive = async () => {
|
||||
setLoading(true);
|
||||
|
||||
await updateConversation({
|
||||
variables: { id: conversation.id, archived: !conversation.archived },
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
||||
{conversation.archived
|
||||
? t("messaging.labels.unarchive")
|
||||
: t("messaging.labels.archive")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Space } from "antd";
|
||||
import React from "react";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<Space wrap>
|
||||
<PhoneNumberFormatter>
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
<ChatArchiveButton conversation={conversation} />
|
||||
</Space>
|
||||
<div>
|
||||
<div className="imex-flex-row">
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
</div>
|
||||
<div className="imex-flex-row">
|
||||
<PhoneNumberFormatter>
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Select, Empty, Space } from "antd";
|
||||
import { Select, Empty } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -13,27 +13,27 @@ export default function ChatTagRoComponent({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Space flex>
|
||||
<div style={{ width: "15rem" }}>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
dropdownMatchSelectWidth
|
||||
placeholder={t("general.labels.search")}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||
item.ownr_ln || ""
|
||||
} ${item.ownr_co_nm || ""}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
style={{
|
||||
width: 300,
|
||||
}}
|
||||
placeholder={t("general.labels.search")}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||
item.ownr_ln || ""
|
||||
} ${item.ownr_co_nm || ""}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{loading ? <LoadingOutlined /> : null}
|
||||
|
||||
{loading ? (
|
||||
@@ -41,6 +41,6 @@ export default function ChatTagRoComponent({
|
||||
) : (
|
||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
|
||||
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -44,8 +44,8 @@ export function ContractsFindModalContainer({
|
||||
|
||||
callSearch({
|
||||
variables: {
|
||||
plate:
|
||||
(values.plate && values.plate !== "" && values.plate) || undefined,
|
||||
fleet:
|
||||
(values.fleet && values.fleet !== "" && values.fleet) || undefined,
|
||||
time: values.time,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,12 +90,13 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
record.cccontracts.length === 1 ? (
|
||||
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
|
||||
{record.cccontracts[0].job.ro_number}
|
||||
</Link>
|
||||
) : null,
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
{record.cccontracts.length === 1
|
||||
? record.cccontracts[0].job.ro_number
|
||||
: null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { Card } from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyEmployeeEfficiency({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_employee_efficiency)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
|
||||
moment(item.date).format("YYYY-MM-DD")
|
||||
);
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
const chartData = listOfDays.reduce((acc, val) => {
|
||||
//Sum up the current day.
|
||||
let dailyHrs;
|
||||
if (!!ticketsByDate[val]) {
|
||||
dailyHrs = ticketsByDate[val].reduce(
|
||||
(dayAcc, dayVal) => {
|
||||
return {
|
||||
actual: dayAcc.actual + dayVal.actualhrs,
|
||||
productive: dayAcc.actual + dayVal.productivehrs,
|
||||
};
|
||||
},
|
||||
{ actual: 0, productive: 0 }
|
||||
);
|
||||
} else {
|
||||
dailyHrs = { actual: 0, productive: 0 };
|
||||
}
|
||||
|
||||
const dailyEfficiency =
|
||||
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.productive + 1) * 100;
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("DD"),
|
||||
...dailyHrs,
|
||||
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
|
||||
accActual:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accActual + dailyHrs.actual
|
||||
: dailyHrs.actual,
|
||||
|
||||
accProductive:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accProductive + dailyHrs.productive
|
||||
: dailyHrs.productive,
|
||||
accEfficiency: 0,
|
||||
};
|
||||
theValue.accEfficiency = (
|
||||
((theValue.accProductive - theValue.accActual) /
|
||||
(theValue.accProductive || 1) +
|
||||
1) *
|
||||
100
|
||||
).toFixed(1);
|
||||
|
||||
return [...acc, theValue];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyemployeeefficiency")}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
orientation="left"
|
||||
stroke="#8884d8"
|
||||
unit=" hrs"
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="#82ca9d"
|
||||
unit="%"
|
||||
/>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
name="Accumulated Efficiency"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="accEfficiency"
|
||||
stroke="#152228"
|
||||
connectNulls
|
||||
// activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
name="Daily Efficiency"
|
||||
yAxisId="right"
|
||||
unit="%"
|
||||
type="monotone"
|
||||
connectNulls
|
||||
dataKey="dailyEfficiency"
|
||||
stroke="#d31717"
|
||||
/>
|
||||
<Bar
|
||||
name="Actual Hours"
|
||||
dataKey="actual"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#102568"
|
||||
/>
|
||||
<Bar
|
||||
name="Productive Hours"
|
||||
dataKey="productive"
|
||||
yAxisId="left"
|
||||
unit=" hrs"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#017664"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyEmployeeEfficiencyGql = `
|
||||
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}},{date: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}} ]}) {
|
||||
actualhrs
|
||||
productivehrs
|
||||
employeeid
|
||||
employee {
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
date
|
||||
}
|
||||
`;
|
||||
@@ -1,163 +0,0 @@
|
||||
import { Card, Input, Space, Table, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../../utils/sorters";
|
||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||
import Dinero from "dinero.js";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [costingData, setcostingData] = useState(null);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function getCostingData() {
|
||||
if (data && data.monthly_sales) {
|
||||
setLoading(true);
|
||||
const response = await axios.post("/job/costingmulti", {
|
||||
jobids: data.monthly_sales.map((x) => x.id),
|
||||
});
|
||||
setcostingData(response.data);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
getCostingData();
|
||||
}, [data]);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: t("bodyshop.fields.responsibilitycenter"),
|
||||
dataIndex: "cost_center",
|
||||
key: "cost_center",
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.sales"),
|
||||
dataIndex: "sales",
|
||||
key: "sales",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.costs"),
|
||||
dataIndex: "costs",
|
||||
key: "costs",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.labels.gpdollars"),
|
||||
dataIndex: "gpdollars",
|
||||
key: "gpdollars",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.gpdollars.substring(1)) -
|
||||
parseFloat(b.gpdollars.substring(1)),
|
||||
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.gppercent"),
|
||||
dataIndex: "gppercent",
|
||||
key: "gppercent",
|
||||
sorter: (a, b) =>
|
||||
parseFloat(a.gppercent.slice(0, -1) || 0) -
|
||||
parseFloat(b.gppercent.slice(0, -1) || 0),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
|
||||
},
|
||||
];
|
||||
const filteredData =
|
||||
searchText === ""
|
||||
? (costingData && costingData.allCostCenterData) || []
|
||||
: costingData.allCostCenterData.filter((d) =>
|
||||
(d.cost_center || "")
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.monthlyjobcosting")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
columns={columns}
|
||||
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "100%" }}
|
||||
dataSource={filteredData}
|
||||
summary={() => (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.totals")}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.totalSales
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.totalCost
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{Dinero(
|
||||
costingData &&
|
||||
costingData.allSummaryData &&
|
||||
costingData.allSummaryData.gpdollars
|
||||
).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell></Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</LoadingSkeleton>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyLaborSales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const laborData = {};
|
||||
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.mod_lbr_ty) return;
|
||||
if (!laborData[jobline.mod_lbr_ty])
|
||||
laborData[jobline.mod_lbr_ty] = Dinero();
|
||||
laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add(
|
||||
Dinero({
|
||||
amount: Math.round(
|
||||
(job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100
|
||||
),
|
||||
}).multiply(jobline.mod_lb_hrs || 0)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(laborData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
|
||||
value: laborData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlylaborsales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
|
||||
const pieColor = (type) => {
|
||||
if (type === "LAA") return "lightgreen";
|
||||
else if (type === "LAB") return "dodgerblue";
|
||||
else if (type === "LAD") return "aliceblue";
|
||||
else if (type === "LAE") return "seafoam";
|
||||
else if (type === "LAG") return "chartreuse";
|
||||
else if (type === "LAF") return "magenta";
|
||||
else if (type === "LAM") return "gold";
|
||||
else if (type === "LAR") return "crimson";
|
||||
else if (type === "LAU") return "slategray";
|
||||
else if (type === "LA1") return "slategray";
|
||||
else if (type === "LA2") return "slategray";
|
||||
else if (type === "LA3") return "slategray";
|
||||
else if (type === "LA4") return "slategray";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
//const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
//midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// // const sx = cx + (outerRadius + 10) * cos;
|
||||
// const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
// const my = cy + (outerRadius + 30) * sin;
|
||||
// //const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
//const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
// <path
|
||||
// d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
|
||||
// stroke={fill}
|
||||
// fill="none"
|
||||
// />;
|
||||
// <text
|
||||
// x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
// y={ey}
|
||||
// textAnchor={textAnchor}
|
||||
// fill="#333"
|
||||
// >
|
||||
// {payload.name}
|
||||
// </text>
|
||||
// <text
|
||||
// x={ex + (cos >= 0 ? 1 : -1) * 12}
|
||||
// y={ey}
|
||||
// dy={18}
|
||||
// textAnchor={textAnchor}
|
||||
// fill="#999"
|
||||
// >
|
||||
// {Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
// </text>
|
||||
@@ -1,136 +0,0 @@
|
||||
import { Card } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyPartsSales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const partData = {};
|
||||
|
||||
data.monthly_sales.forEach((job) => {
|
||||
job.joblines.forEach((jobline) => {
|
||||
if (!jobline.part_type) return;
|
||||
if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero();
|
||||
partData[jobline.part_type] = partData[jobline.part_type].add(
|
||||
Dinero({ amount: Math.round((jobline.act_price || 0) * 100) }).multiply(
|
||||
jobline.part_qty || 0
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const chartData = Object.keys(partData).map((key) => {
|
||||
return {
|
||||
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
|
||||
value: partData[key].getAmount() / 100,
|
||||
color: pieColor(key.toUpperCase()),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart margin={0} padding={0}>
|
||||
<Pie
|
||||
data={chartData}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={renderActiveShape}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius="60%"
|
||||
// outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
const pieColor = (type) => {
|
||||
if (type === "PAA") return "darkgreen";
|
||||
else if (type === "PAC") return "green";
|
||||
else if (type === "PAE") return "gold";
|
||||
else if (type === "PAG") return "seafoam";
|
||||
else if (type === "PAL") return "chartreuse";
|
||||
else if (type === "PAM") return "magenta";
|
||||
else if (type === "PAN") return "crimson";
|
||||
else if (type === "PAO") return "gold";
|
||||
else if (type === "PAP") return "crimson";
|
||||
else if (type === "PAR") return "indigo";
|
||||
else if (type === "PAS") return "dodgerblue";
|
||||
else if (type === "PASL") return "dodgerblue";
|
||||
return "slategray";
|
||||
};
|
||||
|
||||
const renderActiveShape = (props) => {
|
||||
// const RADIAN = Math.PI / 180;
|
||||
const {
|
||||
cx,
|
||||
cy,
|
||||
// midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
fill,
|
||||
payload,
|
||||
// percent,
|
||||
value,
|
||||
} = props;
|
||||
// const sin = Math.sin(-RADIAN * midAngle);
|
||||
// const cos = Math.cos(-RADIAN * midAngle);
|
||||
// const sx = cx + (outerRadius + 10) * cos;
|
||||
//const sy = cy + (outerRadius + 10) * sin;
|
||||
// const mx = cx + (outerRadius + 30) * cos;
|
||||
//const my = cy + (outerRadius + 30) * sin;
|
||||
// const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
||||
// const ey = my;
|
||||
// const textAnchor = cos >= 0 ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
|
||||
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
|
||||
</text>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
fill={fill}
|
||||
/>
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
startAngle={startAngle}
|
||||
endAngle={endAngle}
|
||||
innerRadius={outerRadius + 6}
|
||||
outerRadius={outerRadius + 10}
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
@@ -2,30 +2,29 @@ import { Card } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Area,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import Dinero from "dinero.js";
|
||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||
moment(item.date_invoiced).format("YYYY-MM-DD")
|
||||
);
|
||||
const jobsByDate = {
|
||||
"2020-07-5": [{ clm_total: 1224 }],
|
||||
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }],
|
||||
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }],
|
||||
"2020-07-21": [{ clm_total: 15000 }],
|
||||
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }],
|
||||
};
|
||||
|
||||
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||
|
||||
@@ -34,19 +33,17 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
let dailySales;
|
||||
if (!!jobsByDate[val]) {
|
||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||
return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
|
||||
}, Dinero());
|
||||
return dayAcc + dayVal.clm_total;
|
||||
}, 0);
|
||||
} else {
|
||||
dailySales = Dinero();
|
||||
dailySales = 0;
|
||||
}
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("DD"),
|
||||
dailySales: dailySales.getAmount() / 100,
|
||||
date: moment(val).format("D dd"),
|
||||
dailySales,
|
||||
accSales:
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
|
||||
: dailySales.getAmount() / 100,
|
||||
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
@@ -54,40 +51,32 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||
<div style={{ height: "100%" }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip
|
||||
formatter={(value, name, props) => value && value.toFixed(2)}
|
||||
/>
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#3CB371"
|
||||
stroke="#3CB371"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Sales"
|
||||
dataKey="accSales"
|
||||
fill="#8884d8"
|
||||
stroke="#8884d8"
|
||||
/>
|
||||
<Bar
|
||||
name="Daily Sales"
|
||||
dataKey="dailySales"
|
||||
//stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardMonthlyRevenueGraphGql = `
|
||||
|
||||
`;
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
import { Card, Statistic } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.projected_monthly_sales)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
const aboveTargetMonthlySales = false;
|
||||
|
||||
const dollars =
|
||||
data.projected_monthly_sales &&
|
||||
data.projected_monthly_sales.reduce(
|
||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
return (
|
||||
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.projectedmonthlysales")}
|
||||
value={222000.0}
|
||||
precision={2}
|
||||
prefix={
|
||||
<div>
|
||||
{aboveTargetMonthlySales ? (
|
||||
<ArrowUpOutlined />
|
||||
) : (
|
||||
<ArrowDownOutlined />
|
||||
)}
|
||||
$
|
||||
</div>
|
||||
}
|
||||
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardProjectedMonthlySalesGql = `
|
||||
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}]}) {
|
||||
id
|
||||
date_invoiced
|
||||
job_totals
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function DashboardRefreshRequired(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
<SyncOutlined style={{ fontSize: "300%", margin: "1rem" }} />
|
||||
<div>{t("dashboard.errors.refreshrequired")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,33 @@
|
||||
import { Card, Statistic } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
|
||||
export default function DashboardTotalProductionDollars({
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const dollars =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||
Dinero()
|
||||
);
|
||||
const aboveTargetProductionDollars = false;
|
||||
|
||||
return (
|
||||
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||
<Statistic value={dollars.toFormat()} />
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.productiondollars")}
|
||||
value={175000.0}
|
||||
precision={2}
|
||||
prefix={
|
||||
<div>
|
||||
{aboveTargetProductionDollars ? (
|
||||
<ArrowUpOutlined />
|
||||
) : (
|
||||
<ArrowDownOutlined />
|
||||
)}
|
||||
$
|
||||
</div>
|
||||
}
|
||||
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,19 @@
|
||||
import { Card, Space, Statistic } from "antd";
|
||||
import React from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardTotalProductionHours);
|
||||
|
||||
export function DashboardTotalProductionHours({
|
||||
bodyshop,
|
||||
data,
|
||||
...cardProps
|
||||
}) {
|
||||
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
if (!data) return null;
|
||||
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
|
||||
const hours =
|
||||
data.production_jobs &&
|
||||
data.production_jobs.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
|
||||
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
total:
|
||||
acc.total +
|
||||
val.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
val.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
},
|
||||
{ body: 0, ref: 0, total: 0 }
|
||||
);
|
||||
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
|
||||
const aboveTargetHours = true;
|
||||
return (
|
||||
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||
<Space wrap style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.bodyhrs")}
|
||||
value={hours.body.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.refhrs")}
|
||||
value={hours.ref.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("dashboard.labels.prodhrs")}
|
||||
value={hours.total.toFixed(1)}
|
||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||
/>
|
||||
</Space>
|
||||
<Card {...cardProps}>
|
||||
<Statistic
|
||||
title={t("dashboard.titles.productionhours")}
|
||||
value={750}
|
||||
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardTotalProductionHoursGql = ``;
|
||||
|
||||
@@ -1,355 +1,185 @@
|
||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import DashboardMonthlyEmployeeEfficiency, {
|
||||
DashboardMonthlyEmployeeEfficiencyGql,
|
||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
||||
import DashboardMonthlyRevenueGraph, {
|
||||
DashboardMonthlyRevenueGraphGql,
|
||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||
import DashboardProjectedMonthlySales, {
|
||||
DashboardProjectedMonthlySalesGql,
|
||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||
import DashboardTotalProductionHours, {
|
||||
DashboardTotalProductionHoursGql,
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
//Combination of the following:
|
||||
// /node_modules/react-grid-layout/css/styles.css
|
||||
// /node_modules/react-resizable/css/styles.css
|
||||
import "./dashboard-grid.styles.scss";
|
||||
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||
// import Icon from "@ant-design/icons";
|
||||
// import { Button, Dropdown, Menu, notification } from "antd";
|
||||
// import React, { useState } from "react";
|
||||
// import { useMutation, useQuery } from "@apollo/client";
|
||||
// import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { MdClose } from "react-icons/md";
|
||||
// import { connect } from "react-redux";
|
||||
// import { createStructuredSelector } from "reselect";
|
||||
// import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
|
||||
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||
// import {
|
||||
// selectBodyshop,
|
||||
// selectCurrentUser,
|
||||
// } from "../../redux/user/user.selectors";
|
||||
// import AlertComponent from "../alert/alert.component";
|
||||
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
// //Combination of the following:
|
||||
// // /node_modules/react-grid-layout/css/styles.css
|
||||
// // /node_modules/react-resizable/css/styles.css
|
||||
// import "./dashboard-grid.styles.css";
|
||||
// import "./dashboard-grid.styles.scss";
|
||||
|
||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
// const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
// const mapStateToProps = createStructuredSelector({
|
||||
// currentUser: selectCurrentUser,
|
||||
// bodyshop: selectBodyshop,
|
||||
// });
|
||||
// const mapDispatchToProps = (dispatch) => ({
|
||||
// //setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
// });
|
||||
|
||||
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
...(bodyshop.associations[0].user.dashboardlayout
|
||||
? bodyshop.associations[0].user.dashboardlayout
|
||||
: { items: [], layout: {}, layouts: [] }),
|
||||
});
|
||||
// export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
|
||||
// const { t } = useTranslation();
|
||||
// const [state, setState] = useState({
|
||||
// layout: bodyshop.associations[0].user.dashboardlayout || [
|
||||
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
|
||||
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
|
||||
// ],
|
||||
// });
|
||||
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
createDashboardQuery(state)
|
||||
);
|
||||
// const handleLayoutChange = async (newLayout) => {
|
||||
// logImEXEvent("dashboard_change_layout");
|
||||
// setState({ ...state, layout: newLayout });
|
||||
// const result = await updateLayout({
|
||||
// variables: { email: currentUser.email, layout: newLayout },
|
||||
// });
|
||||
|
||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
// if (!!result.errors) {
|
||||
// notification["error"]({
|
||||
// message: t("dashboard.errors.updatinglayout", {
|
||||
// message: JSON.stringify(result.errors),
|
||||
// }),
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleLayoutChange = async (layout, layouts) => {
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
// const handleRemoveComponent = (key) => {
|
||||
// logImEXEvent("dashboard_remove_component", { name: key });
|
||||
|
||||
setState({ ...state, layout, layouts });
|
||||
// const idxToRemove = state.layout.findIndex((i) => i.i === key);
|
||||
// const newLayout = state.layout;
|
||||
// newLayout.splice(idxToRemove, 1);
|
||||
// handleLayoutChange(newLayout);
|
||||
// };
|
||||
|
||||
const result = await updateLayout({
|
||||
variables: {
|
||||
email: currentUser.email,
|
||||
layout: { ...state, layout, layouts },
|
||||
},
|
||||
});
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("dashboard.errors.updatinglayout", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleRemoveComponent = (key) => {
|
||||
logImEXEvent("dashboard_remove_component", { name: key });
|
||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||
console.log(
|
||||
"🚀 ~ file: dashboard-grid.component.jsx ~ line 81 ~ idxToRemove",
|
||||
idxToRemove
|
||||
);
|
||||
const items = _.cloneDeep(state.items);
|
||||
// const handleAddComponent = (e) => {
|
||||
// logImEXEvent("dashboard_add_component", { name: e });
|
||||
|
||||
items.splice(idxToRemove, 1);
|
||||
setState({ ...state, items });
|
||||
};
|
||||
// handleLayoutChange([
|
||||
// ...state.layout,
|
||||
// {
|
||||
// i: e.key,
|
||||
// x: (state.layout.length * 2) % (state.cols || 12),
|
||||
// y: Infinity, // puts it at the bottom
|
||||
// w: componentList[e.key].w || 2,
|
||||
// h: componentList[e.key].h || 2,
|
||||
// },
|
||||
// ]);
|
||||
// };
|
||||
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", { name: e });
|
||||
setState({
|
||||
...state,
|
||||
items: [
|
||||
...state.items,
|
||||
{
|
||||
i: e.key,
|
||||
x: (state.items.length * 2) % (state.cols || 12),
|
||||
y: 99, // puts it at the bottom
|
||||
w: componentList[e.key].w || 2,
|
||||
h: componentList[e.key].h || 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
// const onBreakpointChange = (breakpoint, cols) => {
|
||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
||||
// };
|
||||
|
||||
const dashboarddata = React.useMemo(
|
||||
() => GenerateDashboardData(data),
|
||||
[data]
|
||||
);
|
||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||
const addComponentOverlay = (
|
||||
<Menu onClick={handleAddComponent}>
|
||||
{Object.keys(componentList).map((key) => (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={existingLayoutKeys.includes(key)}
|
||||
>
|
||||
{componentList[key].label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
// const existingLayoutKeys = state.layout.map((i) => i.i);
|
||||
// const addComponentOverlay = (
|
||||
// <Menu onClick={handleAddComponent}>
|
||||
// {Object.keys(componentList).map((key) => (
|
||||
// <Menu.Item
|
||||
// key={key}
|
||||
// value={key}
|
||||
// disabled={existingLayoutKeys.includes(key)}
|
||||
// >
|
||||
// {componentList[key].label}
|
||||
// </Menu.Item>
|
||||
// ))}
|
||||
// </Menu>
|
||||
// );
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
// if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
// return (
|
||||
// <div>
|
||||
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||
// <Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||
// </Dropdown>
|
||||
// <ResponsiveReactGridLayout
|
||||
// className="layout"
|
||||
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
// width="100%"
|
||||
// onLayoutChange={handleLayoutChange}
|
||||
// onBreakpointChange={onBreakpointChange}
|
||||
// >
|
||||
// {state.layout.map((item, index) => {
|
||||
// const TheComponent = componentList[item.i].component;
|
||||
// return (
|
||||
// <div key={item.i} data-grid={item}>
|
||||
// <LoadingSkeleton loading={loading}>
|
||||
// <Icon
|
||||
// component={MdClose}
|
||||
// key={item.i}
|
||||
// style={{
|
||||
// position: "absolute",
|
||||
// zIndex: "2",
|
||||
// right: ".25rem",
|
||||
// top: ".25rem",
|
||||
// cursor: "pointer",
|
||||
// }}
|
||||
// onClick={() => handleRemoveComponent(item.i)}
|
||||
// />
|
||||
// <TheComponent
|
||||
// className="dashboard-card"
|
||||
// size="small"
|
||||
// style={{ height: "100%", width: "100%" }}
|
||||
// />
|
||||
// </LoadingSkeleton>
|
||||
// </div>
|
||||
// );
|
||||
// })}
|
||||
// </ResponsiveReactGridLayout>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
<ResponsiveReactGridLayout
|
||||
className="layout"
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
width="100%"
|
||||
layouts={state.layouts}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
// onBreakpointChange={onBreakpointChange}
|
||||
>
|
||||
{state.items.map((item, index) => {
|
||||
const TheComponent = componentList[item.i].component;
|
||||
return (
|
||||
<div
|
||||
key={item.i}
|
||||
data-grid={{
|
||||
...item,
|
||||
minH: componentList[item.i].minH || 1,
|
||||
minW: componentList[item.i].minW || 1,
|
||||
}}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Icon
|
||||
component={MdClose}
|
||||
key={item.i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "2",
|
||||
right: ".25rem",
|
||||
top: ".25rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleRemoveComponent(item.i)}
|
||||
/>
|
||||
<TheComponent className="dashboard-card" data={dashboarddata} />
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ResponsiveReactGridLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// export default connect(
|
||||
// mapStateToProps,
|
||||
// mapDispatchToProps
|
||||
// )(DashboardGridComponent);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardGridComponent);
|
||||
|
||||
const componentList = {
|
||||
ProductionDollars: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardTotalProductionDollars,
|
||||
gqlFragment: null,
|
||||
w: 1,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
ProductionHours: {
|
||||
label: i18next.t("dashboard.titles.productionhours"),
|
||||
component: DashboardTotalProductionHours,
|
||||
gqlFragment: DashboardTotalProductionHoursGql,
|
||||
w: 3,
|
||||
h: 1,
|
||||
minW: 3,
|
||||
minH: 1,
|
||||
},
|
||||
ProjectedMonthlySales: {
|
||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||
component: DashboardProjectedMonthlySales,
|
||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||
w: 2,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
MonthlyRevenueGraph: {
|
||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||
component: DashboardMonthlyRevenueGraph,
|
||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||
w: 4,
|
||||
h: 2,
|
||||
minW: 4,
|
||||
minH: 2,
|
||||
},
|
||||
MonthlyJobCosting: {
|
||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||
component: DashboardMonthlyJobCosting,
|
||||
gqlFragment: null,
|
||||
minW: 6,
|
||||
minH: 3,
|
||||
w: 6,
|
||||
h: 3,
|
||||
},
|
||||
MonthlyPartsSales: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardMonthlyPartsSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyLaborSales: {
|
||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||
component: DashboardMonthlyLaborSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyEmployeeEfficency: {
|
||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||
component: DashboardMonthlyEmployeeEfficiency,
|
||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const createDashboardQuery = (state) => {
|
||||
const componentBasedAdditions =
|
||||
state &&
|
||||
Array.isArray(state.layout) &&
|
||||
state.layout
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS {
|
||||
${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD")}"}}]}) {
|
||||
id
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
// const componentList = {
|
||||
// ProductionDollars: {
|
||||
// label: "Production Dollars",
|
||||
// component: DashboardTotalProductionDollars,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// ProductionHours: {
|
||||
// label: "Production Hours",
|
||||
// component: DashboardTotalProductionHours,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// ProjectedMonthlySales: {
|
||||
// label: "Projected Monthly Sales",
|
||||
// component: DashboardProjectedMonthlySales,
|
||||
// w: 2,
|
||||
// h: 1,
|
||||
// },
|
||||
// MonthlyRevenueGraph: {
|
||||
// label: "Monthly Sales Graph",
|
||||
// component: DashboardMonthlyRevenueGraph,
|
||||
// w: 2,
|
||||
// h: 2,
|
||||
// },
|
||||
// };
|
||||
|
||||
128
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
@@ -0,0 +1,128 @@
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: content-box;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||
background-position: bottom right;
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.react-resizable-handle-w,
|
||||
.react-resizable-handle-e {
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.react-resizable-handle-w {
|
||||
left: 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.react-resizable-handle-e {
|
||||
right: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.react-resizable-handle-n,
|
||||
.react-resizable-handle-s {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.react-resizable-handle-n {
|
||||
top: 0;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.react-resizable-handle-s {
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.react-grid-layout {
|
||||
position: relative;
|
||||
transition: height 200ms ease;
|
||||
}
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: red;
|
||||
opacity: 0.2;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-resizable-hide > .react-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,154 +1,12 @@
|
||||
.react-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: content-box;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
|
||||
background-position: bottom right;
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
.react-resizable-handle-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: sw-resize;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.react-resizable-handle-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.react-resizable-handle-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nw-resize;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.react-resizable-handle-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: ne-resize;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.react-resizable-handle-w,
|
||||
.react-resizable-handle-e {
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.react-resizable-handle-w {
|
||||
left: 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.react-resizable-handle-e {
|
||||
right: 0;
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.react-resizable-handle-n,
|
||||
.react-resizable-handle-s {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.react-resizable-handle-n {
|
||||
top: 0;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.react-resizable-handle-s {
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.react-grid-layout {
|
||||
position: relative;
|
||||
transition: height 200ms ease;
|
||||
}
|
||||
.react-grid-item {
|
||||
transition: all 200ms ease;
|
||||
transition-property: left, top;
|
||||
}
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: transform;
|
||||
}
|
||||
.react-grid-item.resizing {
|
||||
z-index: 1;
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.react-grid-item.react-draggable-dragging {
|
||||
transition: none;
|
||||
z-index: 3;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.react-grid-item.dropping {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-grid-item.react-grid-placeholder {
|
||||
background: red;
|
||||
opacity: 0.2;
|
||||
transition-duration: 100ms;
|
||||
z-index: 2;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.4);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-resizable-hide > .react-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
.dashboard-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
// background-color: green;
|
||||
|
||||
.ant-card-body {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
// // background-color: red;
|
||||
// height: 90%;
|
||||
// width: 100%;
|
||||
// padding: 8px;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
}
|
||||
.ant-spin-nested-loading {
|
||||
// background-color: red;
|
||||
height: 100%;
|
||||
.ant-spin-container {
|
||||
height: 100%;
|
||||
.ant-table {
|
||||
height: 100%;
|
||||
.ant-table-container {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function GenerateDashboardData(data) {
|
||||
return data;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
//import "tui-image-editor/dist/tui-image-editor.css";
|
||||
import { Result } from "antd";
|
||||
import * as markerjs2 from "markerjs2";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
||||
const imgRef = useRef(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploaded, setuploaded] = useState(false);
|
||||
const markerArea = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const triggerUpload = useCallback(
|
||||
async (dataUrl) => {
|
||||
setLoading(true);
|
||||
handleUpload(
|
||||
{
|
||||
filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`,
|
||||
file: await b64toBlob(dataUrl),
|
||||
onSuccess: () => {
|
||||
setLoading(false);
|
||||
setuploaded(true);
|
||||
},
|
||||
onError: () => setLoading(false),
|
||||
},
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
uploaded_by: currentUser.email,
|
||||
jobId: document.jobid,
|
||||
//billId: billId,
|
||||
tagsArray: ["edited"],
|
||||
//callback: callbackAfterUpload,
|
||||
}
|
||||
);
|
||||
},
|
||||
[bodyshop, currentUser, document]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (imgRef.current !== null) {
|
||||
// create a marker.js MarkerArea
|
||||
markerArea.current = new markerjs2.MarkerArea(imgRef.current);
|
||||
console.log(`markerArea.current`, markerArea.current);
|
||||
// attach an event handler to assign annotated image back to our image element
|
||||
markerArea.current.addCloseEventListener((closeEvent) => {
|
||||
console.log("Close Event", closeEvent);
|
||||
});
|
||||
|
||||
markerArea.current.addRenderEventListener((dataUrl) => {
|
||||
imgRef.current.src = dataUrl;
|
||||
markerArea.current.close();
|
||||
triggerUpload(dataUrl);
|
||||
});
|
||||
// launch marker.js
|
||||
|
||||
markerArea.current.renderAtNaturalSize = true;
|
||||
markerArea.current.renderImageType = "image/jpeg";
|
||||
markerArea.current.renderImageQuality = 1;
|
||||
//markerArea.current.settings.displayMode = "inline";
|
||||
markerArea.current.show();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [triggerUpload]);
|
||||
|
||||
async function b64toBlob(url) {
|
||||
const res = await fetch(url);
|
||||
return await res.blob();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!loading && !uploaded && (
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={GenerateSrcUrl(document)}
|
||||
alt="sample"
|
||||
crossOrigin="anonymous"
|
||||
style={{ maxWidth: "90vw", maxHeight: "90vh" }}
|
||||
/>
|
||||
)}
|
||||
{loading && <LoadingSpinner message={t("documents.labels.uploading")} />}
|
||||
{uploaded && (
|
||||
<Result
|
||||
status="success"
|
||||
title={t("documents.successes.edituploaded")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DocumentEditorComponent);
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Result } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router";
|
||||
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
|
||||
import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries";
|
||||
import { setBodyshop } from "../../redux/user/user.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import DocumentEditor from "./document-editor.component";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBodyshop: (bs) => dispatch(setBodyshop(bs)),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DocumentEditorContainer);
|
||||
|
||||
export function DocumentEditorContainer({ setBodyshop }) {
|
||||
//Get the image details for the image to be saved.
|
||||
//Get the document id from the search string.
|
||||
const { documentId } = queryString.parse(useLocation().search);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
loading: loadingShop,
|
||||
error: errorShop,
|
||||
data: dataShop,
|
||||
} = useQuery(QUERY_BODYSHOP, {
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (dataShop) setBodyshop(dataShop.bodyshops[0]);
|
||||
}, [dataShop, setBodyshop]);
|
||||
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, {
|
||||
variables: { documentId },
|
||||
skip: !documentId,
|
||||
});
|
||||
|
||||
if (loading || loadingShop) return <LoadingSpinner />;
|
||||
if (error || errorShop)
|
||||
return (
|
||||
<AlertComponent
|
||||
message={error.message || errorShop.message}
|
||||
type="error"
|
||||
/>
|
||||
);
|
||||
|
||||
if (!data || !data.documents_by_pk)
|
||||
return <Result status="404" title={t("general.errors.notfound")} />;
|
||||
return (
|
||||
<div>
|
||||
<DocumentEditor document={data ? data.documents_by_pk : null} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
import exifr from "exifr";
|
||||
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
@@ -21,13 +19,8 @@ export const handleUpload = (ev, context) => {
|
||||
const { onError, onSuccess, onProgress } = ev;
|
||||
const { bodyshop, jobId } = context;
|
||||
|
||||
const fileName = ev.file.name || ev.filename;
|
||||
|
||||
let key = `${bodyshop.id}/${jobId}/${fileName.replace(
|
||||
/\.[^/.]+$/,
|
||||
""
|
||||
)}-${new Date().getTime()}`;
|
||||
let extension = fileName.split(".").pop();
|
||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
||||
let extension = ev.file.name.split(".").pop();
|
||||
uploadToCloudinary(
|
||||
key,
|
||||
extension,
|
||||
@@ -92,7 +85,6 @@ export const uploadToCloudinary = async (
|
||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
@@ -130,16 +122,6 @@ export const uploadToCloudinary = async (
|
||||
}
|
||||
|
||||
//Insert the document with the matching key.
|
||||
let takenat;
|
||||
if (fileType.includes("image")) {
|
||||
try {
|
||||
const exif = await exifr.parse(file);
|
||||
|
||||
takenat = exif && exif.DateTimeOriginal;
|
||||
} catch (error) {
|
||||
console.log("Unable to parse image file for EXIF Data");
|
||||
}
|
||||
}
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
variables: {
|
||||
@@ -150,10 +132,9 @@ export const uploadToCloudinary = async (
|
||||
uploaded_by: uploaded_by,
|
||||
key: key,
|
||||
type: fileType,
|
||||
extension: cloudinaryUploadResponse.data.format || extension,
|
||||
extension: extension,
|
||||
bodyshopid: bodyshop.id,
|
||||
size: cloudinaryUploadResponse.data.bytes || file.size,
|
||||
takenat,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -185,7 +166,6 @@ export const uploadToCloudinary = async (
|
||||
}
|
||||
};
|
||||
|
||||
//Also needs to be updated in media JS and mobile app.
|
||||
export function DetermineFileType(filetype) {
|
||||
if (!filetype) return "auto";
|
||||
else if (filetype.startsWith("image")) return "image";
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||
import { selectEmailConfig } from "../../redux/email/email.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
emailConfig: selectEmailConfig,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(EmailDocumentsComponent);
|
||||
|
||||
export function EmailDocumentsComponent({
|
||||
emailConfig,
|
||||
|
||||
selectedMediaState,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedMedia, setSelectedMedia] = selectedMediaState;
|
||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||
variables: {
|
||||
jobId: emailConfig.jobid,
|
||||
},
|
||||
skip: !emailConfig.jobid,
|
||||
});
|
||||
console.log(
|
||||
"🚀 ~ file: email-documents.component.jsx ~ line 38 ~ emailConfig",
|
||||
emailConfig
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <LoadingSpinner />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { Divider, Form, Input, Select, Tabs, Upload } from "antd";
|
||||
import { Card, Divider, Form, Input, Select, Upload } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmailDocumentsComponent from "../email-documents/email-documents.component";
|
||||
|
||||
export default function EmailOverlayComponent({ form, selectedMediaState }) {
|
||||
export default function EmailOverlayComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -37,8 +36,6 @@ export default function EmailOverlayComponent({ form, selectedMediaState }) {
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t("emails.labels.preview")}</Divider>
|
||||
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
@@ -55,38 +52,34 @@ export default function EmailOverlayComponent({ form, selectedMediaState }) {
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
|
||||
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
|
||||
<Form.Item
|
||||
name="fileList"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={(e) => {
|
||||
if (Array.isArray(e)) {
|
||||
return e;
|
||||
}
|
||||
return e && e.fileList;
|
||||
}}
|
||||
<Card title={t("emails.labels.attachments")}>
|
||||
<Form.Item
|
||||
name="fileList"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={(e) => {
|
||||
console.log("Upload event:", e);
|
||||
if (Array.isArray(e)) {
|
||||
return e;
|
||||
}
|
||||
return e && e.fileList;
|
||||
}}
|
||||
>
|
||||
<Upload.Dragger
|
||||
beforeUpload={Upload.LIST_IGNORE}
|
||||
multiple
|
||||
listType="picture-card"
|
||||
>
|
||||
<Upload.Dragger
|
||||
beforeUpload={Upload.LIST_IGNORE}
|
||||
multiple
|
||||
listType="picture-card"
|
||||
>
|
||||
<>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
Click or drag files to this area to upload.
|
||||
</p>
|
||||
</>
|
||||
</Upload.Dragger>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
Click or drag files to this area to upload.
|
||||
</p>
|
||||
</>
|
||||
</Upload.Dragger>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,12 +43,6 @@ export function EmailOverlayContainer({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [rawHtml, setRawHtml] = useState("");
|
||||
const [pdfCopytoAttach, setPdfCopytoAttach] = useState({
|
||||
filename: null,
|
||||
pdf: null,
|
||||
});
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
|
||||
const defaultEmailFrom = {
|
||||
from: {
|
||||
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
|
||||
@@ -62,18 +56,17 @@ export function EmailOverlayContainer({
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("email_send_from_modal");
|
||||
console.log(`values`, values);
|
||||
const attachments = [];
|
||||
|
||||
//const attachments = [];
|
||||
|
||||
// if (values.fileList)
|
||||
// await asyncForEach(values.fileList, async (f) => {
|
||||
// const t = {
|
||||
// ContentType: f.type,
|
||||
// Filename: f.name,
|
||||
// Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
||||
// };
|
||||
// attachments.push(t);
|
||||
// });
|
||||
await asyncForEach(values.fileList, async (f) => {
|
||||
const t = {
|
||||
ContentType: f.type,
|
||||
Filename: f.name,
|
||||
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
||||
};
|
||||
attachments.push(t);
|
||||
});
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
@@ -81,29 +74,9 @@ export function EmailOverlayContainer({
|
||||
...defaultEmailFrom,
|
||||
...values,
|
||||
html: rawHtml,
|
||||
attachments: [
|
||||
...(values.fileList
|
||||
? await Promise.all(
|
||||
values.fileList.map(async (f) => {
|
||||
return {
|
||||
filename: f.name,
|
||||
path: await toBase64(f.originFileObj),
|
||||
};
|
||||
})
|
||||
)
|
||||
: []),
|
||||
...(pdfCopytoAttach.pdf
|
||||
? [
|
||||
{
|
||||
path: pdfCopytoAttach.pdf,
|
||||
filename:
|
||||
pdfCopytoAttach.filename &&
|
||||
`${pdfCopytoAttach.filename}.pdf`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
|
||||
attachments: await Promise.all(
|
||||
values.fileList.map(async (f) => await toBase64(f.originFileObj))
|
||||
),
|
||||
//attachments,
|
||||
});
|
||||
notification["success"]({ message: t("emails.successes.sent") });
|
||||
@@ -120,22 +93,13 @@ export function EmailOverlayContainer({
|
||||
const render = async () => {
|
||||
logImEXEvent("email_render_template", { template: emailConfig.template });
|
||||
setLoading(true);
|
||||
let { html, pdf, filename } = await RenderTemplate(
|
||||
emailConfig.template,
|
||||
bodyshop,
|
||||
true
|
||||
);
|
||||
let html = await RenderTemplate(emailConfig.template, bodyshop, true);
|
||||
|
||||
const response = await axios.post("/render/inlinecss", {
|
||||
html: html,
|
||||
url: `${window.location.protocol}://${window.location.host}/`,
|
||||
});
|
||||
setRawHtml(response.data);
|
||||
|
||||
if (pdf) {
|
||||
setPdfCopytoAttach({ pdf, filename });
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
...emailConfig.messageOptions,
|
||||
cc:
|
||||
@@ -173,12 +137,7 @@ export function EmailOverlayContainer({
|
||||
<LoadingSpinner message={t("emails.labels.generatingemail")} />
|
||||
</div>
|
||||
)}
|
||||
{!loading && (
|
||||
<EmailOverlayComponent
|
||||
form={form}
|
||||
selectedMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{!loading && <EmailOverlayComponent form={form} />}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
@@ -196,8 +155,8 @@ const toBase64 = (file) =>
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
// const asyncForEach = async (array, callback) => {
|
||||
// for (let index = 0; index < array.length; index++) {
|
||||
// await callback(array[index], index, array);
|
||||
// }
|
||||
// };
|
||||
const asyncForEach = async (array, callback) => {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,14 +5,9 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -39,37 +34,21 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
|
||||
handleErrorSubmit = () => {
|
||||
window.$crisp.push([
|
||||
"do",
|
||||
"message:send",
|
||||
[
|
||||
"text",
|
||||
`I hit the following error: \n\n
|
||||
${this.state.error.message}\n\n
|
||||
${this.state.error.stack}\n\n
|
||||
URL:${window.location} as ${this.props.currentUser.email} for ${
|
||||
this.props.bodyshop && this.props.bodyshop.name
|
||||
}
|
||||
`,
|
||||
],
|
||||
]);
|
||||
const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
||||
|
||||
window.$crisp.push(["do", "chat:open"]);
|
||||
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
||||
----
|
||||
System Generated Log:
|
||||
${this.state.error.message}
|
||||
${this.state.error.stack}
|
||||
`;
|
||||
|
||||
// ----
|
||||
// System Generated Log:
|
||||
// ${this.state.error.message}
|
||||
// ${this.state.error.stack}
|
||||
// `;
|
||||
|
||||
// const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
|
||||
// errorDescription
|
||||
// )}&customfield_10049=${window.location}&email=${
|
||||
// this.props.currentUser.email
|
||||
// }`;
|
||||
// console.log(`URL`, URL);
|
||||
// window.open(URL, "_blank");
|
||||
const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
|
||||
errorDescription
|
||||
)}&customfield_10049=${window.location}&email=${
|
||||
this.props.currentUser.email
|
||||
}`;
|
||||
console.log(`URL`, URL);
|
||||
window.open(URL, "_blank");
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -78,23 +57,6 @@ class ErrorBoundary extends React.Component {
|
||||
if (this.state.hasErrored === true) {
|
||||
logImEXEvent("error_boundary_rendered", { error, info });
|
||||
|
||||
window.$crisp.push([
|
||||
"set",
|
||||
"session:event",
|
||||
[
|
||||
[
|
||||
[
|
||||
"error_boundary",
|
||||
{
|
||||
error: this.state.error.message,
|
||||
stack: this.state.error.stack,
|
||||
},
|
||||
"red",
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
@@ -112,7 +74,7 @@ class ErrorBoundary extends React.Component {
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
<Button onClick={this.handleErrorSubmit}>
|
||||
{t("general.actions.senderrortosupport")}
|
||||
{t("general.actions.submitticket")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
function FeatureWrapper({
|
||||
bodyshop,
|
||||
featureName,
|
||||
noauth,
|
||||
children,
|
||||
...restProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (HasFeatureAccess({ featureName, bodyshop })) return children;
|
||||
|
||||
return (
|
||||
noauth || (
|
||||
<AlertComponent
|
||||
message={t("general.messages.nofeatureaccess")}
|
||||
type="warning"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function HasFeatureAccess({ featureName, bodyshop }) {
|
||||
return (
|
||||
bodyshop.features.allAccess ||
|
||||
moment(bodyshop.features[featureName]).isAfter(moment())
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(FeatureWrapper);
|
||||
|
||||
/*
|
||||
dashboard
|
||||
production-board
|
||||
scoreboard
|
||||
csi
|
||||
tech-console
|
||||
mobile-imaging
|
||||
*/
|
||||
@@ -11,8 +11,9 @@ import AlertComponent from "../alert/alert.component";
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [callSearch, { loading, error, data }] =
|
||||
useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(
|
||||
GLOBAL_SEARCH_QUERY
|
||||
);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
|
||||
@@ -37,7 +38,7 @@ export default function GlobalSearch() {
|
||||
value: job.ro_number,
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${job.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
||||
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
||||
job.ownr_co_nm || ""
|
||||
@@ -45,7 +46,7 @@ export default function GlobalSearch() {
|
||||
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||
job.v_model_desc || ""
|
||||
}`}</span>
|
||||
<span>{`${job.clm_no || ""}`}</span>
|
||||
<span>{`${job.clm_no}`}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
@@ -62,7 +63,7 @@ export default function GlobalSearch() {
|
||||
}`,
|
||||
label: (
|
||||
<Link to={`/manage/owners/${owner.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
|
||||
owner.ownr_co_nm || ""
|
||||
}`}</span>
|
||||
@@ -85,14 +86,14 @@ export default function GlobalSearch() {
|
||||
} ${vehicle.v_model_desc || ""}`,
|
||||
label: (
|
||||
<Link to={`/manage/vehicles/${vehicle.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<span>
|
||||
{`${vehicle.v_model_yr || ""} ${
|
||||
vehicle.v_make_desc || ""
|
||||
} ${vehicle.v_model_desc || ""}`}
|
||||
</span>
|
||||
<span>{vehicle.plate_no || ""}</span>
|
||||
<span> {vehicle.v_vin || ""}</span>
|
||||
<span>{vehicle.plate_no}</span>
|
||||
<span> {vehicle.v_vin}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
@@ -107,12 +108,11 @@ export default function GlobalSearch() {
|
||||
value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`,
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${payment.job.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<span>{payment.paymentnum}</span>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<span>{payment.job.ro_number}</span>
|
||||
<span>{payment.memo || ""}</span>
|
||||
<span>{payment.amount || ""}</span>
|
||||
<span>{payment.transactionid || ""}</span>
|
||||
<span>{payment.job.memo}</span>
|
||||
<span>{payment.job.amount}</span>
|
||||
<span>{payment.job.transactionid}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
@@ -127,7 +127,7 @@ export default function GlobalSearch() {
|
||||
value: `${bill.invoice_number} - ${bill.vendor.name}`,
|
||||
label: (
|
||||
<Link to={`/manage/bills?billid=${bill.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<span>{bill.invoice_number}</span>
|
||||
<span>{bill.vendor.name}</span>
|
||||
<span>{bill.date}</span>
|
||||
@@ -147,7 +147,7 @@ export default function GlobalSearch() {
|
||||
}`,
|
||||
label: (
|
||||
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<Space wrap split={<Divider type="vertical" />}>
|
||||
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
|
||||
pb.company || ""
|
||||
}`}</span>
|
||||
@@ -166,10 +166,10 @@ export default function GlobalSearch() {
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
dropdownMatchSelectWidth={"false"}
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
>
|
||||
<Input.Search loading={loading} />
|
||||
</AutoComplete>
|
||||
|
||||
@@ -3,12 +3,10 @@ import Icon, {
|
||||
BarChartOutlined,
|
||||
CarFilled,
|
||||
ClockCircleFilled,
|
||||
DashboardFilled,
|
||||
DollarCircleFilled,
|
||||
ExportOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileAddFilled,
|
||||
FileAddOutlined,
|
||||
FileFilled,
|
||||
GlobalOutlined,
|
||||
HomeFilled,
|
||||
@@ -16,7 +14,6 @@ import Icon, {
|
||||
LineChartOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
QuestionCircleFilled,
|
||||
ScheduleOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
@@ -47,6 +44,7 @@ import {
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -80,11 +78,12 @@ function Header({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Layout.Header>
|
||||
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
//theme="light"
|
||||
theme={"dark"}
|
||||
style={{ flex: 1 }}
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
@@ -96,7 +95,6 @@ function Header({
|
||||
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="jobssubmenu"
|
||||
icon={<Icon component={FaCarCrash} />}
|
||||
title={t("menus.header.jobs")}
|
||||
>
|
||||
@@ -111,14 +109,12 @@ function Header({
|
||||
{t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="newjob" icon={<FileAddOutlined />}>
|
||||
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div1" />
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div2" />
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
||||
<Link to="/manage/production/list">
|
||||
{t("menus.header.productionlist")}
|
||||
@@ -129,13 +125,13 @@ function Header({
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div3" />
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="customers"
|
||||
icon={<UserOutlined />}
|
||||
title={t("menus.header.customers")}
|
||||
>
|
||||
@@ -147,7 +143,6 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="ccs"
|
||||
icon={<CarFilled />}
|
||||
title={t("menus.header.courtesycars")}
|
||||
>
|
||||
@@ -168,7 +163,6 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="accounting"
|
||||
icon={<DollarCircleFilled />}
|
||||
title={t("menus.header.accounting")}
|
||||
>
|
||||
@@ -190,7 +184,7 @@ function Header({
|
||||
>
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div4" />
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
@@ -202,11 +196,11 @@ function Header({
|
||||
context: null,
|
||||
});
|
||||
}}
|
||||
icon={<Icon component={FaCreditCard} />}
|
||||
>
|
||||
<Icon component={FaCreditCard} />
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div5" />
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
@@ -225,10 +219,9 @@ function Header({
|
||||
>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div6" />
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.SubMenu
|
||||
key="accountingexport"
|
||||
title={t("menus.header.export")}
|
||||
icon={<ExportOutlined />}
|
||||
>
|
||||
@@ -262,17 +255,11 @@ function Header({
|
||||
{t("menus.header.temporarydocs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="shopsubmenu"
|
||||
title={t("menus.header.shop")}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
|
||||
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
|
||||
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
key="reportcenter"
|
||||
icon={<BarChartOutlined />}
|
||||
@@ -298,27 +285,17 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="user"
|
||||
style={{ float: "right" }}
|
||||
title={
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown")
|
||||
}
|
||||
>
|
||||
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
|
||||
<Menu.Item danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="help"
|
||||
onClick={() => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
}}
|
||||
icon={<Icon component={QuestionCircleFilled} />}
|
||||
>
|
||||
{t("menus.header.help")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="rescue"
|
||||
onClick={() => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}}
|
||||
@@ -332,7 +309,6 @@ function Header({
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="langselecter"
|
||||
title={
|
||||
<span>
|
||||
<GlobalOutlined />
|
||||
@@ -351,7 +327,7 @@ function Header({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
|
||||
<Menu.SubMenu style={{ float: "right" }} title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</Link>
|
||||
@@ -359,6 +335,9 @@ function Header({
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
<div>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
</Layout.Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export default function JiraSupportComponent() {
|
||||
useScript();
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const useScript = () => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://jsd-widget.atlassian.com/assets/embed.js";
|
||||
script.setAttribute("data-jsd-embedded", true);
|
||||
script.setAttribute("data-key", "d69bb65c-1dd3-483f-b109-66a970d03f44");
|
||||
script.setAttribute("data-base-url", "https://jsd-widget.atlassian.com");
|
||||
//script.async = true;
|
||||
script.onload = () => {
|
||||
var DOMContentLoaded_event = document.createEvent("Event");
|
||||
DOMContentLoaded_event.initEvent("DOMContentLoaded", true, true);
|
||||
window.document.dispatchEvent(DOMContentLoaded_event);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -87,7 +87,9 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
|
||||
<Button type="primary" onClick={showModal}>
|
||||
{t("printcenter.jobs.3rdpartypayer")}
|
||||
</Button>
|
||||
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
@@ -161,7 +163,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("printcenter.jobs.3rdpartyfields.refnumber")}
|
||||
label={t("printcenter.jobs.3rdpartyfields.ponumber")}
|
||||
name="ponumber"
|
||||
>
|
||||
<Input />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, Popover, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
@@ -11,8 +11,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import queryString from "query-string";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
@@ -26,8 +24,6 @@ export function ScheduleEventComponent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const history = useHistory();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
|
||||
const blockContent = (
|
||||
<div>
|
||||
@@ -92,20 +88,6 @@ export function ScheduleEventComponent({
|
||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
{event.job ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: event.job.id,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("appointments.actions.preview")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const Template = TemplateList("job").appointment_reminder;
|
||||
@@ -115,8 +97,7 @@ export function ScheduleEventComponent({
|
||||
variables: { id: event.job.id },
|
||||
},
|
||||
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
|
||||
"e",
|
||||
event.job && event.job.id
|
||||
"e"
|
||||
);
|
||||
}}
|
||||
disabled={event.arrived}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "../../../../redux/user/user.selectors";
|
||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||
import moment from "moment-business-days";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -82,7 +81,6 @@ export function JobChecklistForm({
|
||||
|
||||
...(type === "deliver" && {
|
||||
scheduled_delivery: values.scheduled_delivery,
|
||||
actual_delivery: values.actual_delivery,
|
||||
}),
|
||||
...(type === "deliver" &&
|
||||
values.removeFromProduction && {
|
||||
@@ -135,20 +133,12 @@ export function JobChecklistForm({
|
||||
initialValues={{
|
||||
...(type === "intake" && {
|
||||
addToProduction: true,
|
||||
scheduled_completion:
|
||||
(job && job.scheduled_completion) ||
|
||||
moment().businessAdd(
|
||||
(job.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
job.larhrs.aggregate.sum.mod_lb_hrs) /
|
||||
bodyshop.target_touchtime,
|
||||
"days"
|
||||
),
|
||||
scheduled_completion: job && job.scheduled_completion,
|
||||
scheduled_delivery: job && job.scheduled_delivery,
|
||||
}),
|
||||
...(type === "deliver" && {
|
||||
removeFromProduction: true,
|
||||
actual_completion: job && job.actual_completion,
|
||||
actual_delivery: job && job.actual_delivery,
|
||||
}),
|
||||
...formItems
|
||||
.filter((fi) => fi.value)
|
||||
@@ -181,21 +171,21 @@ export function JobChecklistForm({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scheduled_delivery"
|
||||
label={t("jobs.fields.scheduled_delivery")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["production_vars", "note"]}
|
||||
label={t("jobs.fields.production_vars.note")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Input.TextArea rows={3} disabled={readOnly} />
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
@@ -212,14 +202,7 @@ export function JobChecklistForm({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="actual_delivery"
|
||||
label={t("jobs.fields.actual_delivery")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
<DateTimePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="removeFromProduction"
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
|
||||
export default function JobChecklistDisplay({ checklist }) {
|
||||
console.log("JobChecklistDisplay -> checklist", checklist);
|
||||
if (!checklist) return <div></div>;
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
|
||||
bordered
|
||||
dataSource={data.notes}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ whiteSpace: "pre-line" }}>
|
||||
<List.Item>
|
||||
{item.critical ? (
|
||||
<EyeInvisibleFilled style={{ margin: 4, color: "red" }} />
|
||||
) : null}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
FilterFilled,
|
||||
SyncOutlined,
|
||||
WarningFilled,
|
||||
EditFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import {
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
Table,
|
||||
Tag,
|
||||
} from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -24,7 +22,6 @@ import { createStructuredSelector } from "reselect";
|
||||
import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
@@ -40,7 +37,6 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
jobRO: selectJobReadOnly,
|
||||
technician: selectTechnician,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -52,7 +48,6 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export function JobLinesComponent({
|
||||
jobRO,
|
||||
technician,
|
||||
setPartsOrderContext,
|
||||
loading,
|
||||
refetch,
|
||||
@@ -160,16 +155,7 @@ export function JobLinesComponent({
|
||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||
<span
|
||||
style={{ marginLeft: ".2rem" }}
|
||||
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -288,31 +274,27 @@ export function JobLinesComponent({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
{t("general.actions.edit")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={async () => {
|
||||
await deleteJobLine({
|
||||
onClick={() =>
|
||||
deleteJobLine({
|
||||
variables: { joblineId: record.id },
|
||||
// update(cache) {
|
||||
// cache.modify({
|
||||
// id: cache.identify(job),
|
||||
// fields: {
|
||||
// joblines(existingJobLines, { readField }) {
|
||||
// return existingJobLines.filter(
|
||||
// (jlRef) => record.id !== readField("id", jlRef)
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
});
|
||||
await axios.post("/job/totalsssu", {
|
||||
id: job.id,
|
||||
});
|
||||
refetch && refetch();
|
||||
}}
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify(job),
|
||||
fields: {
|
||||
joblines(existingJobLines, { readField }) {
|
||||
return existingJobLines.filter(
|
||||
(jlRef) => record.id !== readField("id", jlRef)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
@@ -331,12 +313,9 @@ export function JobLinesComponent({
|
||||
if (e.key === "clear") {
|
||||
setSelectedLines([]);
|
||||
} else {
|
||||
const markedTypes = [e.key];
|
||||
if (e.key === "PAN") markedTypes.push("PAP");
|
||||
if (e.key === "PAS") markedTypes.push("PASL");
|
||||
setSelectedLines([
|
||||
...selectedLines,
|
||||
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
||||
...jobLines.filter((item) => item.part_type === e.key),
|
||||
]);
|
||||
}
|
||||
};
|
||||
@@ -375,8 +354,7 @@ export function JobLinesComponent({
|
||||
disabled={
|
||||
(job && !job.converted) ||
|
||||
(selectedLines.length > 0 ? false : true) ||
|
||||
jobRO ||
|
||||
technician
|
||||
jobRO
|
||||
}
|
||||
onClick={() => {
|
||||
setPartsOrderContext({
|
||||
@@ -400,7 +378,7 @@ export function JobLinesComponent({
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: {
|
||||
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL"],
|
||||
part_type: ["PAN,PAL,PAA,PAS,PASL"],
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -411,7 +389,7 @@ export function JobLinesComponent({
|
||||
<Button>{t("jobs.actions.mark")}</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
disabled={jobRO || technician}
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
setJobLineEditContext({
|
||||
actions: { refetch: refetch },
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JoblinePresetButton({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelect = (item) => {
|
||||
form.setFieldsValue(item);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("joblines.labels.presets")} <DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JoblinePresetButton);
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
|
||||
|
||||
export default function JobLinesUpsertModalComponent({
|
||||
visible,
|
||||
jobLine,
|
||||
@@ -32,7 +32,6 @@ export default function JobLinesUpsertModalComponent({
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
onCancel={handleCancel}
|
||||
e
|
||||
>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
@@ -42,9 +41,6 @@ export default function JobLinesUpsertModalComponent({
|
||||
form={form}
|
||||
>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.line_no")} name="line_no">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.line_desc")}
|
||||
rules={[
|
||||
@@ -57,7 +53,6 @@ export default function JobLinesUpsertModalComponent({
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<JoblinesPreset form={form} />
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||
@@ -128,7 +123,7 @@ export default function JobLinesUpsertModalComponent({
|
||||
// }),
|
||||
// ]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
<InputCurrency />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
@@ -215,13 +210,6 @@ export default function JobLinesUpsertModalComponent({
|
||||
>
|
||||
<InputCurrency precision={2} min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.prt_dsmk_p")}
|
||||
name="prt_dsmk_p"
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber precision={0} min={0} max={100} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||
import Axios from "axios";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobLineEditModal: selectJobLineEditModal,
|
||||
});
|
||||
@@ -29,10 +29,10 @@ function JobLinesUpsertModalContainer({
|
||||
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const handleFinish = (values) => {
|
||||
setLoading(true);
|
||||
if (!jobLineEditModal.context.id) {
|
||||
const r = await insertJobLine({
|
||||
insertJobLine({
|
||||
variables: {
|
||||
lineInput: [
|
||||
{
|
||||
@@ -44,44 +44,42 @@ function JobLinesUpsertModalContainer({
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!r.errors) {
|
||||
await Axios.post("/job/totalsssu", {
|
||||
id: jobLineEditModal.context.jobid,
|
||||
})
|
||||
.then((r) => {
|
||||
if (jobLineEditModal.actions.refetch)
|
||||
jobLineEditModal.actions.refetch();
|
||||
//Need to recalcuate totals.
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("joblines.successes.created"),
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("joblines.errors.creating", {
|
||||
message: error.message,
|
||||
}),
|
||||
});
|
||||
});
|
||||
if (jobLineEditModal.actions.refetch)
|
||||
jobLineEditModal.actions.refetch();
|
||||
//Need to recalcuate totals.
|
||||
toggleModalVisible();
|
||||
notification["success"]({
|
||||
message: t("joblines.successes.created"),
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("joblines.errors.creating", {
|
||||
message: JSON.stringify(r.errors.message),
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const r = await updateJobLine({
|
||||
updateJobLine({
|
||||
variables: {
|
||||
lineId: jobLineEditModal.context.id,
|
||||
line: values,
|
||||
},
|
||||
});
|
||||
if (!r.errors) {
|
||||
notification["success"]({
|
||||
message: t("joblines.successes.updated"),
|
||||
})
|
||||
.then((r) => {
|
||||
notification["success"]({
|
||||
message: t("joblines.successes.updated"),
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["success"]({
|
||||
message: t("joblines.errors.updating", {
|
||||
message: error.message,
|
||||
}),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("joblines.errors.updating", {
|
||||
message: JSON.stringify(r.errors.message),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (jobLineEditModal.actions.submit) {
|
||||
jobLineEditModal.actions.submit();
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button, Card, Space, Table } from "antd";
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import Dinero from "dinero.js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -9,8 +8,8 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { 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";
|
||||
@@ -41,14 +40,14 @@ export function JobPayments({
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
title: t("payments.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.payer"),
|
||||
@@ -116,29 +115,15 @@ export function JobPayments({
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<Button
|
||||
disabled={record.exportedat}
|
||||
onClick={() => {
|
||||
setPaymentContext({
|
||||
actions: { refetch: refetch },
|
||||
context: record,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
<PrintWrapperComponent
|
||||
templateObject={{
|
||||
name: TemplateList("payment").payment_receipt.key,
|
||||
variables: { id: record.id },
|
||||
}}
|
||||
messageObject={{
|
||||
to: job.ownr_ea,
|
||||
}}
|
||||
id={job.id}
|
||||
/>
|
||||
</Space>
|
||||
<PrintWrapperComponent
|
||||
templateObject={{
|
||||
name: TemplateList("payment").payment_receipt.key,
|
||||
variables: { id: record.id },
|
||||
}}
|
||||
messageObject={{
|
||||
to: job.ownr_ea,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -169,7 +154,7 @@ export function JobPayments({
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button
|
||||
disabled={!job.converted}
|
||||
disabled={jobRO}
|
||||
onClick={() =>
|
||||
setPaymentContext({
|
||||
actions: { refetch: refetch },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Checkbox, Table, Typography } from "antd";
|
||||
import { Checkbox, PageHeader, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
@@ -21,7 +21,6 @@ export default function JobReconciliationBillsTable({
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
width: "35%",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
@@ -30,8 +29,6 @@ export default function JobReconciliationBillsTable({
|
||||
title: t("billlines.labels.from"),
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
width: "20%",
|
||||
ellipsis: true,
|
||||
render: (text, record) =>
|
||||
`${record.bill.vendor && record.bill.vendor.name} / ${
|
||||
record.bill.invoice_number
|
||||
@@ -60,7 +57,7 @@ export default function JobReconciliationBillsTable({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_qty"),
|
||||
title: t("billlines.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
@@ -89,12 +86,10 @@ export default function JobReconciliationBillsTable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<PageHeader title={t("bills.labels.bills")}>
|
||||
<Table
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ y: "80vh", x: true }}
|
||||
scroll={{ y: "40vh", x: true }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={invoiceLineData}
|
||||
@@ -104,6 +99,6 @@ export default function JobReconciliationBillsTable({
|
||||
selectedRowKeys: selectedLines,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,23 +22,21 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<JobReconciliationPartsTable
|
||||
jobLineData={jobLineData}
|
||||
jobLineState={jobLineState}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<JobReconciliationBillsTable
|
||||
invoiceLineData={invoiceLineData}
|
||||
billLineState={billLineState}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<JobReconciliationPartsTable
|
||||
jobLineData={jobLineData}
|
||||
jobLineState={jobLineState}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<JobReconciliationBillsTable
|
||||
invoiceLineData={invoiceLineData}
|
||||
billLineState={billLineState}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<JobReconciliationTotals
|
||||
jobLines={jobLineData}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.imex-reconciliation-modal {
|
||||
top: 20px;
|
||||
.ant-modal-content {
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-modal-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { selectReconciliation } from "../../redux/modals/modals.selectors";
|
||||
import JobReconciliationModalComponent from "./job-reconciliation-modal.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import "./job-reconciliation-modal.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reconciliationModal: selectReconciliation,
|
||||
@@ -39,23 +38,23 @@ function JobReconciliationModalContainer({
|
||||
return (
|
||||
<Modal
|
||||
title={t("jobs.labels.reconciliationheader")}
|
||||
width={"95%"}
|
||||
width={"90%"}
|
||||
visible={visible}
|
||||
okText={t("general.actions.close")}
|
||||
onOk={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
cancelButtonProps={{ display: "none" }}
|
||||
destroyOnClose
|
||||
className="imex-reconciliation-modal"
|
||||
>
|
||||
{loading && <LoadingSpinner loading={loading} />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{data && (
|
||||
<JobReconciliationModalComponent
|
||||
job={data && data.jobs_by_pk}
|
||||
bills={data && data.bills}
|
||||
/>
|
||||
)}
|
||||
<LoadingSpinner loading={loading}>
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{data && (
|
||||
<JobReconciliationModalComponent
|
||||
job={data && data.jobs_by_pk}
|
||||
bills={data && data.bills}
|
||||
/>
|
||||
)}
|
||||
</LoadingSpinner>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Table, Typography } from "antd";
|
||||
import { PageHeader, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
@@ -102,13 +102,11 @@ export default function JobReconcilitionPartsTable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>{t("jobs.labels.lines")}</Typography.Title>
|
||||
<PageHeader title={t("jobs.labels.lines")}>
|
||||
<Table
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
size="small"
|
||||
scroll={{ y: "80vh", x: true }}
|
||||
scroll={{ y: "40vh", x: true }}
|
||||
rowKey="id"
|
||||
dataSource={jobLineData}
|
||||
onChange={handleTableChange}
|
||||
@@ -124,6 +122,6 @@ export default function JobReconcilitionPartsTable({
|
||||
<div style={{ fontStyle: "italic", margin: "4px" }}>
|
||||
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Empty, Select, Space, Tag } from "antd";
|
||||
import { Empty, Select } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { forwardRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -80,20 +80,13 @@ const JobSearchSelect = (
|
||||
{theOptions
|
||||
? theOptions.map((o) => (
|
||||
<Option key={o.id} value={o.id} status={o.status}>
|
||||
<Space align="center">
|
||||
<span>
|
||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
||||
o.ro_number || t("general.labels.na")
|
||||
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
||||
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
||||
}| ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
||||
o.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
<Tag>
|
||||
<strong>{o.status}</strong>
|
||||
</Tag>
|
||||
</Space>
|
||||
{`${clm_no ? `${o.clm_no} | ` : ""}${
|
||||
o.ro_number || t("general.labels.na")
|
||||
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
||||
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
||||
}| ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
||||
o.v_model_desc || ""
|
||||
}`}
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
|
||||
@@ -69,28 +69,17 @@ export default function JobTotalsTableParts({ job }) {
|
||||
x: true,
|
||||
}}
|
||||
summary={() => (
|
||||
<>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
{t("jobs.labels.prt_dsmk_total")}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<strong>{t("jobs.labels.partstotal")}</strong>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<strong>{t("jobs.labels.partstotal")}</strong>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>
|
||||
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||
</strong>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</>
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>
|
||||
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||
</strong>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, Menu, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||
|
||||
export function JobsAdminStatus({ bodyshop, job }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||
const updateJobStatus = (status) => {
|
||||
mutationUpdateJobstatus({
|
||||
variables: { jobId: job.id, status: status },
|
||||
})
|
||||
.then((r) => {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({ message: t("jobs.errors.saving") });
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = (
|
||||
<Menu
|
||||
onClick={(e) => {
|
||||
updateJobStatus(e.key);
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
||||
<Menu.Item key={item}>{item}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
|
||||
<Button shape="round">
|
||||
<span>{job.status}</span>
|
||||
|
||||
<DownCircleFilled />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ mutation UNVOID_JOB($jobId: uuid!) {
|
||||
}
|
||||
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
|
||||
currentUser.email
|
||||
}", text: "${t("jobs.labels.unvoidnote")}"}) {
|
||||
}", text: "${t("jobs.labels.unvoidnote", { email: currentUser.email })}"}) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
query: GET_ALL_JOBLINES_BY_PK,
|
||||
variables: { id: jobId },
|
||||
});
|
||||
|
||||
const existingLines = _.cloneDeep(existingLinesFromDb);
|
||||
const linesToInsert = [];
|
||||
const linesToUpdate = [];
|
||||
@@ -20,14 +19,11 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
const matchingIndex = existingLines.findIndex(
|
||||
(eL) => eL.unq_seq === newLine.unq_seq
|
||||
);
|
||||
|
||||
//Should do a check to make sure there is only 1 matching unq sequence number.
|
||||
|
||||
if (matchingIndex >= 0) {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: existingLines[matchingIndex].id,
|
||||
newData: { ...newLine, removed: false },
|
||||
newData: newLine,
|
||||
});
|
||||
|
||||
//Splice out item we found for performance.
|
||||
|
||||
@@ -104,10 +104,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
||||
).data;
|
||||
|
||||
let existingVehicles;
|
||||
if (
|
||||
estData.data.available_jobs_by_pk.est_data.vehicle &&
|
||||
estData.data.available_jobs_by_pk.est_data.vin
|
||||
) {
|
||||
if (estData.data.available_jobs_by_pk.est_data.vehicle) {
|
||||
//There's vehicle data, need to double check the VIN.
|
||||
existingVehicles = await client.query({
|
||||
query: SEARCH_VEHICLE_BY_VIN,
|
||||
@@ -127,7 +124,10 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
||||
data: {
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.importnote"),
|
||||
text: t("jobs.labels.importnote", {
|
||||
date: moment().format("MM/DD/yyy"),
|
||||
time: moment().format("hh:mm a"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
queued_for_parts: true,
|
||||
@@ -278,7 +278,10 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
||||
jobid: selectedJob,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.supplementnote"),
|
||||
text: t("jobs.labels.supplementnote", {
|
||||
date: moment().format("MM/DD/yyy"),
|
||||
time: moment().format("hh:mm a"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import { Collapse, Form, Input, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -11,8 +11,7 @@ import FormItemPhone, {
|
||||
PhoneItemFormatterValidation,
|
||||
} from "../form-items-formatted/phone-form-item.component";
|
||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
||||
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
|
||||
import { JobsDetailRatesParts } from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -27,6 +26,12 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
const { getFieldValue } = form;
|
||||
return (
|
||||
<div>
|
||||
<JobsDetailRatesParts
|
||||
jobRO={false}
|
||||
expanded
|
||||
required={selected && true}
|
||||
form={form}
|
||||
/>
|
||||
<Collapse defaultActiveKey="insurance">
|
||||
<Collapse.Panel
|
||||
key="insurance"
|
||||
@@ -52,13 +57,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||
<Select>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||
<Input />
|
||||
@@ -188,7 +187,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
header={t("menus.jobsdetail.financials")}
|
||||
>
|
||||
<JobsDetailRatesChangeButton form={form} />
|
||||
<JobsMarkPstExempt form={form} />
|
||||
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||
<CurrencyInput />
|
||||
@@ -241,26 +240,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.federal_tax_rate")}
|
||||
name="federal_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.state_tax_rate")}
|
||||
name="state_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.local_tax_rate")}
|
||||
name="local_tax_rate"
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||
<CurrencyInput />
|
||||
@@ -333,12 +312,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
||||
</LayoutFormRow>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<JobsDetailRatesParts
|
||||
jobRO={false}
|
||||
expanded
|
||||
required={selected && true}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React, { useContext } from "react";
|
||||
import { SEARCH_VEHICLES } from "../../graphql/vehicles.queries";
|
||||
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
||||
import { useQuery } from "@apollo/client";
|
||||
|
||||
export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||
const [state] = useContext(JobCreateContext);
|
||||
const { loading, error, data } = useQuery(SEARCH_VEHICLES, {
|
||||
variables: { search: `%${state.vehicle.search}%` },
|
||||
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, {
|
||||
variables: { vin: `%${state.vehicle.search}%` },
|
||||
skip: !state.vehicle.search,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||
return (
|
||||
<JobsCreateVehicleInfoComponent
|
||||
loading={loading}
|
||||
vehicles={data ? data.search_vehicles : null}
|
||||
vehicles={data ? data.vehicles : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,12 +83,24 @@ export default function JobsCreateVehicleInfoNewComponent() {
|
||||
<Form.Item
|
||||
label={t("vehicles.fields.plate_st")}
|
||||
name={["vehicle", "data", "plate_st"]}
|
||||
rules={[
|
||||
{
|
||||
required: state.vehicle.new,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={!state.vehicle.new} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("vehicles.fields.plate_no")}
|
||||
name={["vehicle", "data", "plate_no"]}
|
||||
rules={[
|
||||
{
|
||||
required: state.vehicle.new,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled={!state.vehicle.new} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatePicker, Form, Statistic, Tooltip } from "antd";
|
||||
import { DatePicker, Form, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -23,10 +23,6 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
);
|
||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
||||
|
||||
const calcRepairDays =
|
||||
job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0) /
|
||||
(bodyshop.target_touchtime === 0 ? 1 : bodyshop.target_touchtime);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormRow header={t("jobs.forms.estdates")}>
|
||||
@@ -56,17 +52,6 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||
<DateTimePicker disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Tooltip
|
||||
title={t("jobs.labels.calc_repair_days_tt", {
|
||||
target_touchtime: bodyshop.target_touchtime,
|
||||
})}
|
||||
>
|
||||
<Statistic
|
||||
value={calcRepairDays}
|
||||
precision={1}
|
||||
title={t("jobs.labels.calc_repair_days")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormRow>
|
||||
<FormRow header={t("jobs.forms.repairdates")}>
|
||||
<Form.Item
|
||||
|
||||
@@ -54,13 +54,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||
<Select disabled={jobRO}>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||
<Input disabled={jobRO} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -156,7 +156,7 @@ export function JobsDetailHeaderActions({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
disabled={!job.converted}
|
||||
disabled={jobRO || !job.converted}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_enter_payment");
|
||||
|
||||
@@ -181,7 +181,7 @@ export function JobsDetailHeaderActions({
|
||||
{job.inproduction ? (
|
||||
<Menu.Item
|
||||
key="addtoproduction"
|
||||
disabled={!job.converted}
|
||||
disabled={!!!job.converted || jobRO}
|
||||
onClick={() => AddToProduction(client, job.id, refetch, true)}
|
||||
>
|
||||
{t("jobs.actions.removefromproduction")}
|
||||
@@ -189,7 +189,7 @@ export function JobsDetailHeaderActions({
|
||||
) : (
|
||||
<Menu.Item
|
||||
key="addtoproduction"
|
||||
disabled={!job.converted}
|
||||
disabled={!!!job.converted || !!job.inproduction || jobRO}
|
||||
onClick={() => AddToProduction(client, job.id, refetch)}
|
||||
>
|
||||
{t("jobs.actions.addtoproduction")}
|
||||
@@ -201,7 +201,7 @@ export function JobsDetailHeaderActions({
|
||||
? t("production.labels.alertoff")
|
||||
: t("production.labels.alerton")}
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
|
||||
<Menu.SubMenu title={t("menus.jobsactions.duplicate")}>
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -317,7 +317,6 @@ export function JobsDetailHeaderActions({
|
||||
{t("menus.jobsactions.admin")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<JobsDetailHeaderActionsExportcustdataComponent job={job} />
|
||||
<JobsDetaiLheaderCsi job={job} />
|
||||
<Menu.Item
|
||||
key="jobcosting"
|
||||
@@ -386,7 +385,10 @@ export function JobsDetailHeaderActions({
|
||||
jobid: job.id,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.voidnote"),
|
||||
text: t("jobs.labels.voidnote", {
|
||||
date: moment().format("MM/DD/yyy"),
|
||||
time: moment().format("hh:mm a"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser'
|
||||
@@ -98,7 +97,6 @@ export function JobsDetailHeaderCsi({
|
||||
}
|
||||
if (e.key === "email")
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
@@ -141,7 +139,6 @@ export function JobsDetailHeaderCsi({
|
||||
} else {
|
||||
if (e.key === "email")
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
@@ -180,11 +177,8 @@ export function JobsDetailHeaderCsi({
|
||||
}
|
||||
};
|
||||
|
||||
if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return <></>;
|
||||
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
key="sendcsi"
|
||||
title={t("jobs.actions.sendcsi")}
|
||||
disabled={!job.converted}
|
||||
{...props}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_NEW_JOB, QUERY_JOB_FOR_DUPE } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
INSERT_NEW_JOB,
|
||||
QUERY_ALL_JOB_FIELDS,
|
||||
} from "../../graphql/jobs.queries";
|
||||
|
||||
export default async function DuplicateJob(
|
||||
apolloClient,
|
||||
@@ -15,21 +18,33 @@ export default async function DuplicateJob(
|
||||
const { defaultOpenStatus } = config;
|
||||
//get a list of all fields on the job
|
||||
const res = await apolloClient.query({
|
||||
query: QUERY_JOB_FOR_DUPE,
|
||||
query: QUERY_ALL_JOB_FIELDS,
|
||||
variables: { id: jobId },
|
||||
});
|
||||
|
||||
console.log("res", res);
|
||||
const { jobs_by_pk } = res.data;
|
||||
const existingJob = _.cloneDeep(jobs_by_pk);
|
||||
delete existingJob.__typename;
|
||||
delete existingJob.id;
|
||||
delete existingJob.createdat;
|
||||
delete existingJob.updatedat;
|
||||
const { jobs_by_pk: existingJob } = res.data;
|
||||
|
||||
const newJob = {
|
||||
...existingJob,
|
||||
date_estimated: new Date(),
|
||||
shopid: existingJob.shopid,
|
||||
status: defaultOpenStatus,
|
||||
ownerid: existingJob.ownerid,
|
||||
ownr_fn: existingJob.ownr_fn,
|
||||
ownr_ln: existingJob.ownr_ln,
|
||||
ownr_co_nm: existingJob.ownr_co_nm,
|
||||
ownr_addr1: existingJob.ownr_addr1,
|
||||
ownr_addr2: existingJob.ownr_addr2,
|
||||
ownr_st: existingJob.ownr_st,
|
||||
ownr_zip: existingJob.ownr_zip,
|
||||
ownr_ctry: existingJob.ownr_ctry,
|
||||
|
||||
ownr_ph1: existingJob.ownr_ph1,
|
||||
vehicleid: existingJob.vehicleid,
|
||||
v_vin: existingJob.v_vin,
|
||||
v_make_desc: existingJob.v_make_desc,
|
||||
v_model_desc: existingJob.v_model_desc,
|
||||
v_model_yr: existingJob.v_model_yr,
|
||||
};
|
||||
|
||||
const _tempLines = _.cloneDeep(existingJob.joblines);
|
||||
@@ -39,20 +54,24 @@ export default async function DuplicateJob(
|
||||
line.manual_line = true;
|
||||
});
|
||||
newJob.joblines = keepJobLines ? _tempLines : [];
|
||||
newJob.job_totals = (
|
||||
await Axios.post("/job/totals", {
|
||||
job: newJob,
|
||||
})
|
||||
).data;
|
||||
|
||||
delete newJob.joblines;
|
||||
newJob.joblines = keepJobLines ? { data: _tempLines } : null;
|
||||
|
||||
const res2 = await apolloClient.mutate({
|
||||
mutation: INSERT_NEW_JOB,
|
||||
variables: { job: [newJob] },
|
||||
});
|
||||
await Axios.post("/job/totalsssu", {
|
||||
id: res2.data.insert_jobs.returning[0].id,
|
||||
});
|
||||
|
||||
if (completionCallback)
|
||||
completionCallback(res2.data.insert_jobs.returning[0].id);
|
||||
apolloClient
|
||||
.mutate({
|
||||
mutation: INSERT_NEW_JOB,
|
||||
variables: { job: [newJob] },
|
||||
})
|
||||
.then((res2) => {
|
||||
if (completionCallback)
|
||||
completionCallback(res2.data.insert_jobs.returning[0].id);
|
||||
});
|
||||
|
||||
//insert the new job. call the callback with the returned ID when done.
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { Menu, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobsDetailHeaderActionexportCustomerData({
|
||||
bodyshop,
|
||||
job,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExportCustData = async (e) => {
|
||||
logImEXEvent("job_export_cust_data");
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/receivables",
|
||||
{ jobIds: [job.id], custDataOnly: true },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log("handle -> XML", QbXmlResponse);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let PartnerResponse;
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
QbXmlResponse.data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting-partner"),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
//Check to see if any of them failed. If they didn't don't execute the update.
|
||||
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
||||
if (failedTransactions.length > 0) {
|
||||
//Uh oh. At least one was no good.
|
||||
failedTransactions.forEach((ft) => {
|
||||
//insert failed export log
|
||||
notification.open({
|
||||
// key: "failedexports",
|
||||
type: "error",
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: ft.errorMessage || "",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
//Handle Failures.
|
||||
} else {
|
||||
//Insert success export log.
|
||||
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
onClick={handleExportCustData}
|
||||
key="exportcustdata"
|
||||
disabled={!job.converted}
|
||||
>
|
||||
{t("jobs.actions.exportcustdata")}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsDetailHeaderActionexportCustomerData);
|
||||
@@ -151,21 +151,17 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
job.vehicle ? (
|
||||
<Link
|
||||
to={
|
||||
disabled
|
||||
? "#"
|
||||
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
|
||||
}
|
||||
>
|
||||
{`${job.v_model_yr || ""} ${job.v_color || ""}
|
||||
<Link
|
||||
to={
|
||||
disabled
|
||||
? "#"
|
||||
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
|
||||
}
|
||||
>
|
||||
{`${job.v_model_yr || ""} ${job.v_color || ""}
|
||||
${job.v_make_desc || ""}
|
||||
${job.v_model_desc || ""}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { Form, Input, InputNumber, Select, Space, Switch, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -16,7 +7,6 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
||||
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
|
||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||
import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
||||
|
||||
@@ -46,11 +36,8 @@ export function JobsDetailRates({ jobRO, form, job }) {
|
||||
<CurrencyInput
|
||||
disabled={jobRO}
|
||||
max={
|
||||
Math.round(
|
||||
(job.job_totals &&
|
||||
job.job_totals.totals.federal_tax.amount) ||
|
||||
0
|
||||
) / 100
|
||||
((job.job_totals && job.job_totals.totals.federal_tax.amount) ||
|
||||
0) / 100
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -113,19 +100,8 @@ export function JobsDetailRates({ jobRO, form, job }) {
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
<Divider
|
||||
orientation="left"
|
||||
type="horizontal"
|
||||
style={{ marginTop: ".8rem", float: "right" }}
|
||||
>
|
||||
{t("jobs.forms.laborrates")}
|
||||
</Divider>
|
||||
<Space>
|
||||
<div></div>
|
||||
<JobsDetailRatesChangeButton form={form} disabled={jobRO} />
|
||||
<JobsMarkPstExempt form={form} />
|
||||
</Space>
|
||||
<FormRow noDivider>
|
||||
<JobsDetailRatesChangeButton form={form} disabled={jobRO} />
|
||||
<FormRow header={t("jobs.forms.laborrates")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.labor_rate_desc")}
|
||||
name="labor_rate_desc"
|
||||
@@ -201,6 +177,7 @@ export function JobsDetailRates({ jobRO, form, job }) {
|
||||
<CurrencyInput disabled={jobRO} />
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
|
||||
<JobsDetailRatesParts form={form} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,11 +19,7 @@ export function JobsDetailRatesParts({
|
||||
|
||||
return (
|
||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||
<Collapse.Panel
|
||||
forceRender
|
||||
header={t("jobs.labels.parts_tax_rates")}
|
||||
key="rates"
|
||||
>
|
||||
<Collapse.Panel header={t("jobs.labels.parts_tax_rates")} key="rates">
|
||||
<LayoutFormRow header={t("joblines.fields.part_types.PAA")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.parts_tax_rates.prt_discp")}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
|
||||
export const GenerateSrcUrl = (value) => {
|
||||
let extension = value.extension;
|
||||
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
|
||||
|
||||
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
|
||||
value.type
|
||||
)}/upload/${value.key}${extension ? `.${extension}` : ""}`;
|
||||
};
|
||||
|
||||
export const GenerateThumbUrl = (value) => {
|
||||
let extension = value.extension;
|
||||
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
|
||||
else if (
|
||||
DetermineFileType(value.type) !== "image" ||
|
||||
(value.type && value.type.includes("application"))
|
||||
)
|
||||
extension = "jpg";
|
||||
|
||||
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
|
||||
value.type
|
||||
)}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${
|
||||
value.key
|
||||
}${extension ? `.${extension}` : ""}`;
|
||||
};
|
||||
@@ -1,87 +1,30 @@
|
||||
import { Button, Space } from "antd";
|
||||
import { Button } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
|
||||
export default function JobsDocumentsDownloadButton({
|
||||
galleryImages,
|
||||
identifier,
|
||||
}) {
|
||||
export default function JobsDocumentsDownloadButton({ galleryImages }) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
const imagesToDownload = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
// ...galleryImages.other.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected),
|
||||
];
|
||||
|
||||
const handleDownload = () => {
|
||||
logImEXEvent("jobs_documents_download");
|
||||
|
||||
axios
|
||||
.post("/media/download", {
|
||||
ids: imagesToDownload.map((_) => _.key),
|
||||
})
|
||||
.then((r) => {
|
||||
// window.open(r.data);
|
||||
downloadAs(
|
||||
r.data,
|
||||
`${identifier || "documents"}.zip`,
|
||||
(progressEvent) => {
|
||||
setDownload((currentDownloadState) => {
|
||||
return {
|
||||
downloaded: progressEvent.loaded || 0,
|
||||
speed:
|
||||
(progressEvent.loaded || 0) -
|
||||
((currentDownloadState && currentDownloadState.downloaded) ||
|
||||
0),
|
||||
};
|
||||
});
|
||||
},
|
||||
() => setDownload(null)
|
||||
);
|
||||
window.open(r.data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
loading={!!download}
|
||||
disabled={imagesToDownload.length < 1}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Space>
|
||||
<span>{t("documents.actions.download")}</span>
|
||||
{download && (
|
||||
<span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(
|
||||
download.speed
|
||||
)} / second)`}</span>
|
||||
)}
|
||||
</Space>
|
||||
</Button>
|
||||
</>
|
||||
<Button disabled={imagesToDownload.length < 1} onClick={handleDownload}>
|
||||
{t("documents.actions.download")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const downloadAs = (url, name, onDownloadProgress, onCompleted) => {
|
||||
cleanAxios
|
||||
.get(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
responseType: "blob",
|
||||
onDownloadProgress: onDownloadProgress,
|
||||
})
|
||||
.then((response) => {
|
||||
onCompleted && onCompleted();
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error", err);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Button } from "antd";
|
||||
import { FileExcelFilled } from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Gallery from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
||||
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
||||
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
||||
@@ -18,30 +17,11 @@ function JobsDocumentsComponent({
|
||||
billId,
|
||||
billsCallback,
|
||||
totalSize,
|
||||
downloadIdentifier,
|
||||
bodyshop,
|
||||
ignoreSizeLimit,
|
||||
}) {
|
||||
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
|
||||
const { t } = useTranslation();
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const onCurrentImageChange = (index) => {
|
||||
setIndex(index);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log("Added event listening for reteching.");
|
||||
// window.addEventListener("storage", (ev) => {
|
||||
// if (ev.key === "refetch" && ev.newValue === true) {
|
||||
// refetch && refetch();
|
||||
// localStorage.setItem("refetch", false);
|
||||
// }
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener("storage");
|
||||
// };
|
||||
// }, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
let documents = data.reduce(
|
||||
@@ -49,14 +29,19 @@ function JobsDocumentsComponent({
|
||||
const fileType = DetermineFileType(value.type);
|
||||
if (value.type.startsWith("image")) {
|
||||
acc.images.push({
|
||||
src: GenerateSrcUrl(value),
|
||||
thumbnail: GenerateThumbUrl(value),
|
||||
src: `${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
||||
}/${DetermineFileType(value.type)}/upload/${value.key}`,
|
||||
thumbnail: `${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
||||
}/${DetermineFileType(value.type)}/upload/${
|
||||
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
|
||||
}/${value.key}`,
|
||||
thumbnailHeight: 225,
|
||||
thumbnailWidth: 225,
|
||||
isSelected: false,
|
||||
key: value.key,
|
||||
extension: value.extension,
|
||||
|
||||
id: value.id,
|
||||
type: value.type,
|
||||
size: value.size,
|
||||
@@ -65,18 +50,24 @@ function JobsDocumentsComponent({
|
||||
} else {
|
||||
let thumb;
|
||||
switch (fileType) {
|
||||
case "video":
|
||||
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/c_fill,f_png,h_250,w_250/${value.key}`;
|
||||
break;
|
||||
case "raw":
|
||||
thumb = `${window.location.origin}/file.png`;
|
||||
break;
|
||||
default:
|
||||
thumb = GenerateThumbUrl(value);
|
||||
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}`;
|
||||
break;
|
||||
}
|
||||
|
||||
const fileName = value.key.split("/").pop();
|
||||
acc.other.push({
|
||||
source: GenerateSrcUrl(value),
|
||||
src: "",
|
||||
src: `${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
||||
}/${fileType}/upload/${fileType === "video" ? "q_auto/" : ""}${
|
||||
value.key
|
||||
}${fileType === "raw" ? `.${value.extension}` : ""}`,
|
||||
thumbnail: thumb,
|
||||
tags: [
|
||||
{
|
||||
@@ -102,7 +93,6 @@ function JobsDocumentsComponent({
|
||||
thumbnailHeight: 225,
|
||||
thumbnailWidth: 225,
|
||||
isSelected: false,
|
||||
|
||||
extension: value.extension,
|
||||
key: value.key,
|
||||
id: value.id,
|
||||
@@ -123,17 +113,11 @@ function JobsDocumentsComponent({
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch && refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<JobsDocumentsGallerySelectAllComponent
|
||||
galleryImages={galleryImages}
|
||||
setGalleryImages={setgalleryImages}
|
||||
/>
|
||||
<JobsDocumentsDownloadButton
|
||||
galleryImages={galleryImages}
|
||||
identifier={downloadIdentifier}
|
||||
/>
|
||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} />
|
||||
<JobsDocumentsDeleteButton
|
||||
galleryImages={galleryImages}
|
||||
deletionCallback={billsCallback || refetch}
|
||||
@@ -160,26 +144,6 @@ function JobsDocumentsComponent({
|
||||
<Gallery
|
||||
images={galleryImages.images}
|
||||
backdropClosesModal={true}
|
||||
currentImageWillChange={onCurrentImageChange}
|
||||
customControls={[
|
||||
<Button
|
||||
key="edit-button"
|
||||
style={{
|
||||
float: "right",
|
||||
zIndex: "5",
|
||||
}}
|
||||
onClick={() => {
|
||||
const newWindow = window.open(
|
||||
`${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>,
|
||||
]}
|
||||
onClickImage={(props) => {
|
||||
window.open(
|
||||
props.target.src,
|
||||
@@ -209,12 +173,11 @@ function JobsDocumentsComponent({
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
};
|
||||
}}
|
||||
onClickThumbnail={(index) => {
|
||||
window.open(
|
||||
galleryImages.other[index].source,
|
||||
galleryImages.other[index].src,
|
||||
"_blank",
|
||||
"toolbar=0,location=0,menubar=0"
|
||||
);
|
||||
|
||||
@@ -23,7 +23,6 @@ export default function JobsDocumentsContainer({
|
||||
return (
|
||||
<JobDocuments
|
||||
data={(data && data.documents) || documentsList || []}
|
||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||
billId={billId}
|
||||
jobId={jobId}
|
||||
|
||||
@@ -5,7 +5,9 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { DELETE_DOCUMENTS } from "../../graphql/documents.queries";
|
||||
import { DELETE_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
export default function JobsDocumentsDeleteButton({
|
||||
@@ -13,57 +15,73 @@ export default function JobsDocumentsDeleteButton({
|
||||
deletionCallback,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [deleteDocument] = useMutation(DELETE_DOCUMENTS);
|
||||
const [deleteDocument] = useMutation(DELETE_DOCUMENT);
|
||||
const imagesToDelete = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected),
|
||||
];
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDelete = () => {
|
||||
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
|
||||
setLoading(true);
|
||||
const res = await axios.post("/media/delete", {
|
||||
ids: imagesToDelete,
|
||||
});
|
||||
imagesToDelete.forEach((image) => {
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
let public_id = image.key;
|
||||
|
||||
const successfulDeletes = [];
|
||||
res.data.forEach((resType) => {
|
||||
Object.keys(resType.deleted).forEach((key) => {
|
||||
if (resType.deleted[key] !== "deleted") {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting_cloudinary", {
|
||||
message: JSON.stringify(resType.deleted[key]),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
const delres = await deleteDocument({
|
||||
variables: {
|
||||
ids: imagesToDelete
|
||||
.filter((i) => successfulDeletes.includes(i.key))
|
||||
.map((i) => i.id),
|
||||
},
|
||||
});
|
||||
if (delres.errors) {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting", {
|
||||
message: JSON.stringify(delres.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification.open({
|
||||
key: "docdeletedsuccesfully",
|
||||
type: "success",
|
||||
message: t("documents.successes.delete"),
|
||||
});
|
||||
axios
|
||||
.post("/media/sign", {
|
||||
public_id: public_id,
|
||||
timestamp: timestamp,
|
||||
})
|
||||
.then((response) => {
|
||||
var signature = response.data;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
||||
formData.append("public_id", public_id);
|
||||
formData.append("timestamp", timestamp);
|
||||
formData.append("signature", signature);
|
||||
|
||||
if (deletionCallback) deletionCallback();
|
||||
}
|
||||
cleanAxios
|
||||
.post(
|
||||
`${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT_API
|
||||
}/${DetermineFileType(image.type)}/destroy`,
|
||||
formData,
|
||||
options
|
||||
)
|
||||
.then((response) => {
|
||||
deleteDocument({ variables: { id: image.id } })
|
||||
.then((r) => {
|
||||
notification.open({
|
||||
key: "docdeletedsuccesfully",
|
||||
type: "success",
|
||||
message: t("documents.successes.delete"),
|
||||
});
|
||||
|
||||
if (deletionCallback) deletionCallback();
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
});
|
||||
//Delete it from our database.
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting_cloudinary", {
|
||||
message: JSON.stringify(error),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Gallery from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
|
||||
function JobsDocumentGalleryExternal({
|
||||
data,
|
||||
@@ -15,8 +15,14 @@ function JobsDocumentGalleryExternal({
|
||||
let documents = data.reduce((acc, value) => {
|
||||
if (value.type.startsWith("image")) {
|
||||
acc.push({
|
||||
src: GenerateSrcUrl(value),
|
||||
thumbnail: GenerateThumbUrl(value),
|
||||
src: `${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
||||
}/${DetermineFileType(value.type)}/upload/${value.key}`,
|
||||
thumbnail: `${
|
||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
||||
}/${DetermineFileType(value.type)}/upload/${
|
||||
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
|
||||
}/${value.key}`,
|
||||
thumbnailHeight: 225,
|
||||
thumbnailWidth: 225,
|
||||
isSelected: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -31,7 +32,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
render: (text, record) => (
|
||||
@@ -43,15 +44,15 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "ownr_ln",
|
||||
key: "ownr_ln",
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
// sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
width: "25%",
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
// sortOrder: sortcolumn === "owner" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid}>
|
||||
return record.owner ? (
|
||||
<Link to={"/manage/owners/" + record.owner.id}>
|
||||
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
|
||||
record.ownr_co_nm || ""
|
||||
}`}
|
||||
@@ -79,7 +80,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "status",
|
||||
width: "10%",
|
||||
ellipsis: true,
|
||||
sorter: true, // (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
@@ -116,7 +117,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "plate_no",
|
||||
width: "8%",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sortOrder: sortcolumn === "plate_no" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? record.plate_no : "";
|
||||
@@ -128,7 +129,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "clm_no",
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? (
|
||||
@@ -149,7 +150,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
width: "10%",
|
||||
sorter: true, //(a, b) => a.clm_total - b.clm_total,
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_total ? (
|
||||
@@ -172,7 +173,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.column && sorter.column.key;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
if (filters.status) {
|
||||
search.statusFilters = JSON.stringify(_.flattenDeep(filters.status));
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Popconfirm, Button } from "antd";
|
||||
import React from "react";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import _ from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
|
||||
export function JobsMarkPstExempt({ jobRO, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleConfirm = () => {
|
||||
const newPartRates = _.cloneDeep(form.getFieldValue("parts_tax_rates"));
|
||||
|
||||
Object.keys(newPartRates).forEach((key) => {
|
||||
newPartRates[key] = {
|
||||
...newPartRates[key],
|
||||
prt_tax_in: false,
|
||||
prt_tax_rt: 0,
|
||||
};
|
||||
});
|
||||
|
||||
form.setFieldsValue({
|
||||
state_tax_rate: 0,
|
||||
tax_lbr_rt: 0,
|
||||
tax_levies_rt: 0,
|
||||
tax_sub_rt: 0,
|
||||
tax_shop_mat_rt: 0,
|
||||
tax_paint_mat_rt: 0,
|
||||
tax_str_rt: 0,
|
||||
tax_tow_rt: 0,
|
||||
parts_tax_rates: newPartRates,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
onConfirm={handleConfirm}
|
||||
disabled={jobRO}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
title={t("jobs.actions.markpstexemptconfirm")}
|
||||
>
|
||||
<Button type="link" disabled={jobRO}>
|
||||
{t("jobs.actions.markpstexempt")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(JobsMarkPstExempt);
|
||||
@@ -57,9 +57,6 @@ export function JobNotesComponent({
|
||||
dataIndex: "text",
|
||||
key: "text",
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<span style={{ whiteSpace: "pre-line" }}>{text}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||