Merge branch 'master' into feature/intellipay

This commit is contained in:
Patrick Fic
2023-08-09 19:48:33 -07:00
325 changed files with 19181 additions and 8389 deletions

View File

@@ -1,14 +1,3 @@
Yarn Dependency Management:
To force upgrades for some packages:
yarn upgrade-interactive --latest
To Start Hasura CLI:
npx hasura console
Migrating to Staging:
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
NGROK TEsting: NGROK TEsting:
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000" ./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
@@ -21,4 +10,4 @@ hasura migrate apply --version "1620771761757" --skip-execution --endpoint https
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#' hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
Generate the license file: Generate the license file:
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite $ generate-license-file --input package.json --output third-party-licenses.txt --overwrite

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,6 @@ REACT_APP_CLOUDINARY_API_KEY=957865933348715
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/ REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

View File

@@ -1,3 +1,4 @@
GENERATE_SOURCEMAP=false
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GA_CODE=231103507 REACT_APP_GA_CODE=231103507

View File

@@ -4,69 +4,71 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.9", "@apollo/client": "^3.7.9",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.5", "@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.3.3", "@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.28.1", "@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.28.1", "@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.6.0", "@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8", "@tanem/react-nprogress": "^5.0.8",
"antd": "^4.22.3", "antd": "^4.24.8",
"apollo-link-logger": "^2.0.0", "apollo-link-logger": "^2.0.1",
"axios": "^0.27.2", "axios": "^1.3.4",
"craco-less": "^1.20.0", "craco-less": "^2.0.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"enquire-js": "^0.2.1", "enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^9.9.1", "firebase": "^9.17.1",
"graphql": "^16.5.0", "graphql": "^16.6.0",
"i18next": "^21.8.14", "i18next": "^22.4.10",
"i18next-browser-languagedetector": "^6.1.4", "i18next-browser-languagedetector": "^7.0.1",
"jsoneditor": "^9.9.0", "jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0", "jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.9", "libphonenumber-js": "^1.10.21",
"logrocket": "^3.0.1", "logrocket": "^3.0.1",
"markerjs2": "^2.22.0", "markerjs2": "^2.28.1",
"moment-business-days": "^1.2.0", "moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34", "moment-timezone": "^0.5.41",
"normalize-url": "^7.0.3", "normalize-url": "^8.0.0",
"phone": "^3.1.23", "phone": "^3.1.35",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^7.1.1", "query-string": "^7.1.3",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^17.0.2", "react": "^17.0.2",
"react-big-calendar": "^1.5.0", "react-big-calendar": "^1.6.8",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1", "react-drag-listview": "^0.2.1",
"react-grid-gallery": "^0.5.5", "react-grid-gallery": "^1.0.0",
"react-grid-layout": "^1.3.4", "react-grid-layout": "^1.3.4",
"react-i18next": "^11.18.1", "react-i18next": "^12.2.0",
"react-icons": "^4.4.0", "react-icons": "^4.7.1",
"react-number-format": "^4.9.3", "react-image-lightbox": "^5.1.4",
"react-redux": "^7.2.8", "react-intersection-observer": "^9.4.3",
"react-number-format": "^5.1.3",
"react-redux": "^8.0.5",
"react-resizable": "^3.0.4", "react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3", "react-scripts": "^5.0.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.3",
"recharts": "^2.1.12", "recharts": "^2.4.3",
"redux": "^4.2.0", "redux": "^4.2.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.1.3", "redux-saga": "^1.2.2",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^4.1.6", "reselect": "^4.1.7",
"sass": "^1.54.0", "sass": "^1.58.3",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.6.1",
"styled-components": "^5.3.5", "styled-components": "^5.3.6",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3", "workbox-background-sync": "^6.5.3",

View File

@@ -143,13 +143,28 @@
} }
} }
//Update row highlighting on production board.
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td { .ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important; background: #e7f3ff !important;
} }
.job-line-manual{ .ant-table-tbody > tr.ant-table-row-selected > td {
background: #e6f7ff !important;
}
.job-line-manual {
color: tomato; color: tomato;
font-style: italic; font-style: italic;
} }
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;
}
.rowWithColor > td {
background-color: var(--bgColor) !important;
}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill }) { export default function BillDeleteButton({ bill, callback }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +36,8 @@ export default function BillDeleteButton({ bill }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
if (callback && typeof callback === "function") callback(bill.id);
} else { } else {
//Check if it's an fkey violation. //Check if it's an fkey violation.
const error = JSON.stringify(result.errors); const error = JSON.stringify(result.errors);

View File

@@ -1,18 +1,18 @@
import { useApolloClient, useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Checkbox, Form, Modal, notification, Space } from "antd"; import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
import _ from "lodash"; import _ from "lodash";
import React, { useEffect, useState, useMemo } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries"; import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { import {
QUERY_JOB_LBR_ADJUSTMENTS, QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB, UPDATE_JOB,
} from "../../graphql/jobs.queries"; } from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries"; import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -20,15 +20,15 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import useLocalStorage from "../../utils/useLocalStorage";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal, billEnterModal: selectBillEnterModal,
@@ -126,6 +126,17 @@ function BillEnterModalContainer({
deductedfromlbr: deductedfromlbr, deductedfromlbr: deductedfromlbr,
lbr_adjustment, lbr_adjustment,
joblineid: i.joblineid === "noline" ? null : i.joblineid, joblineid: i.joblineid === "noline" ? null : i.joblineid,
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) ||
false,
state:
(i.applicable_taxes && i.applicable_taxes.state) ||
false,
local:
(i.applicable_taxes && i.applicable_taxes.local) ||
false,
},
}; };
}), }),
}, },

View File

@@ -1,6 +1,6 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { MdOpenInNew } from "react-icons/md"; import { useTreatments } from "@splitsoftware/splitio-react";
import { import {
Alert, Alert,
Divider, Divider,
@@ -12,14 +12,17 @@ import {
Switch, Switch,
Upload, Upload,
} from "antd"; } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -28,8 +31,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -58,6 +59,11 @@ export function BillFormComponent({
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
@@ -259,6 +265,37 @@ export function BillFormComponent({
required: true, required: true,
//message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
({ getFieldValue }) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value)
.startOf("day")
.isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value)
.startOf("day")
.isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]} ]}
> >
<FormDatePicker disabled={disabled} /> <FormDatePicker disabled={disabled} />

View File

@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors"; import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component"; import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss"; import "./breadcrumbs.styles.scss";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs, breadcrumbs: selectBreadcrumbs,
@@ -15,6 +17,12 @@ const mapStateToProps = createStructuredSelector({
}); });
export function BreadCrumbs({ breadcrumbs, bodyshop }) { export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
return ( return (
<Row className="breadcrumb-container"> <Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}> <Col xs={24} sm={24} md={16}>
@@ -38,7 +46,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop }) {
</Breadcrumb> </Breadcrumb>
</Col> </Col>
<Col xs={24} sm={24} md={8}> <Col xs={24} sm={24} md={8}>
<GlobalSearch /> {OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
</Col> </Col>
</Row> </Row>
); );

View File

@@ -10,7 +10,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_ca_bc_pvrt_calculate"); logImEXEvent("job_ca_bc_pvrt_calculate");
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) }); form.setFieldsValue({
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
});
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
setVisibility(false); setVisibility(false);
}; };

View File

@@ -1,12 +1,19 @@
import { Badge, List, Tag } from "antd"; import { Badge, List, Tag } from "antd";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -18,59 +25,95 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setSelectedConversation(conversationId)), dispatch(setSelectedConversation(conversationId)),
}); });
export function ChatConversationListComponent({ function ChatConversationListComponent({
conversationList, conversationList,
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
loadMoreConversations,
}) { }) {
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
>
<div
style={{
display: "inline-block",
}}
>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div style={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
);
};
return ( return (
<div className="chat-list-container"> <div className="chat-list-container">
<List <AutoSizer>
bordered {({ height, width }) => (
dataSource={conversationList} <VirtualizedList
renderItem={(item) => ( height={height}
<List.Item width={width}
key={item.id} rowCount={conversationList.length}
onClick={() => setSelectedConversation(item.id)} rowHeight={cache.rowHeight}
className={`chat-list-item ${ rowRenderer={rowRenderer}
item.id === selectedConversation onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
? "chat-list-selected-conversation" if (scrollTop + clientHeight === scrollHeight) {
: null loadMoreConversations();
}`} }
> }}
<div sryle={{ display: "inline-block" }}> />
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div sryle={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
)} )}
/> </AutoSizer>
</div> </div>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps

View File

@@ -3,8 +3,9 @@
} }
.chat-list-container { .chat-list-container {
flex: 1; flex: 1;
overflow: auto; overflow: hidden;
height: 100%; height: 100%;
border: 1px solid gainsboro;
} }
.chat-list-item { .chat-list-item {
@@ -21,4 +22,6 @@
.ro-number-tag { .ro-number-tag {
align-self: baseline; align-self: baseline;
} }
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
} }

View File

@@ -42,6 +42,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
MARK_MESSAGES_AS_READ_BY_CONVERSATION, MARK_MESSAGES_AS_READ_BY_CONVERSATION,
{ {
variables: { conversationId: selectedConversation }, variables: { conversationId: selectedConversation },
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
update(cache) { update(cache) {
cache.modify({ cache.modify({
id: cache.identify({ id: cache.identify({

View File

@@ -4,13 +4,16 @@ import {
ShrinkOutlined, ShrinkOutlined,
SyncOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries"; import {
CONVERSATION_LIST_QUERY,
UNREAD_CONVERSATION_COUNT,
} from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import { import {
selectChatVisible, selectChatVisible,
@@ -37,12 +40,21 @@ export function ChatPopupComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0); const [pollInterval, setpollInterval] = useState(0);
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
...(pollInterval > 0 ? { pollInterval } : {}), ...(pollInterval > 0 ? { pollInterval } : {}),
}); });
const [getConversations, { loading, data, refetch, fetchMore }] =
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
});
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
useEffect(() => { useEffect(() => {
@@ -54,15 +66,24 @@ export function ChatPopupComponent({
}, [fcmToken]); }, [fcmToken]);
useEffect(() => { useEffect(() => {
if (called && chatVisible) refetch(); if (chatVisible)
}, [chatVisible, called, refetch]); getConversations({
variables: {
offset: 0,
},
});
}, [chatVisible, getConversations]);
const unreadCount = data const loadMoreConversations = useCallback(() => {
? data.conversations.reduce( if (data)
(acc, val) => val.messages_aggregate.aggregate.count + acc, fetchMore({
0 variables: {
) offset: data.conversations.length,
: 0; },
});
}, [data, fetchMore]);
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
return ( return (
<Badge count={unreadCount}> <Badge count={unreadCount}>
@@ -97,6 +118,7 @@ export function ChatPopupComponent({
) : ( ) : (
<ChatConversationListComponent <ChatConversationListComponent
conversationList={data ? data.conversations : []} conversationList={data ? data.conversations : []}
loadMoreConversations={loadMoreConversations}
/> />
)} )}
</Col> </Col>

View File

@@ -59,6 +59,14 @@ export default function ContractsCarsComponent({
sortOrder: sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order, state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
}, },
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{ {
title: t("courtesycars.fields.plate"), title: t("courtesycars.fields.plate"),
dataIndex: "plate", dataIndex: "plate",
@@ -93,6 +101,9 @@ export default function ContractsCarsComponent({
(cc.model || "") (cc.model || "")
.toLowerCase() .toLowerCase()
.includes(state.search.toLowerCase()) || .includes(state.search.toLowerCase()) ||
(cc.color || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase()) (cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
); );

View File

@@ -4,7 +4,7 @@ import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component"; //import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component"; import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component"; import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
@@ -165,7 +165,9 @@ export default function ContractFormComponent({
/> />
</div> </div>
)} )}
<ContractLicenseDecodeButton form={form} /> {
//<ContractLicenseDecodeButton form={form} />
}
</Space> </Space>
</div> </div>
<LayoutFormRow header={t("contracts.labels.driverinformation")}> <LayoutFormRow header={t("contracts.labels.driverinformation")}>

View File

@@ -0,0 +1,235 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
if (!data) return null;
if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />;
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
});
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
};
return (
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={appt}
/>
</div>
</Card>
);
}
export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
.startOf("day")
.toISOString()}", _lte: "${moment()
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled
id
job {
alt_transport
clm_no
jobid: id
ins_co_nm
iouparent
ownerid
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
production_vars
ro_number
suspended
v_make_desc
v_model_desc
v_model_yr
v_vin
vehicleid
}
note
start
title
}
`;

View File

@@ -0,0 +1,210 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
if (!data) return null;
if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => {
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
});
data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
});
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={data.scheduled_out_today}
/>
</div>
</Card>
);
}
export const DashboardScheduledOutTodayGql = `
scheduled_out_today: jobs(where: {
date_invoiced: {_is_null: true},
ro_number: {_is_null: false},
voided: {_eq: false},
scheduled_completion: {_gte: "${moment().startOf("day").toISOString()}",
_lte: "${moment().endOf("day").toISOString()}"}}) {
alt_transport
clm_no
jobid: id
ins_co_nm
iouparent
ownerid
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
production_vars
ro_number
scheduled_completion
suspended
v_make_desc
v_model_desc
v_model_yr
v_vin
vehicleid
}
`;

View File

@@ -1,6 +1,6 @@
import Icon, { SyncOutlined } from "@ant-design/icons"; import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client"; import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd"; import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
@@ -37,6 +37,12 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following: //Combination of the following:
// /node_modules/react-grid-layout/css/styles.css // /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css // /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, {
DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import "./dashboard-grid.styles.scss"; import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils"; import { GenerateDashboardData } from "./dashboard-grid.utils";
@@ -268,6 +274,28 @@ const componentList = {
w: 2, w: 2,
h: 2, h: 2,
}, },
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
}; };
const createDashboardQuery = (state) => { const createDashboardQuery = (state) => {
@@ -283,8 +311,12 @@ const createDashboardQuery = (state) => {
monthly_sales: jobs(where: {_and: [ monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}}, { voided: {_eq: false}},
{date_invoiced: {_gte: "${moment() {date_invoiced: {_gte: "${moment()
.startOf("month").startOf('day').toISOString()}"}}, {date_invoiced: {_lte: "${moment() .startOf("month")
.endOf("month").endOf('day').toISOString()}"}}]}) { .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
id id
ro_number ro_number
date_invoiced date_invoiced

View File

@@ -66,7 +66,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
key: "status", key: "status",
}, },
{ {
title: t("jobs.fields.ro_number"), title: t("bills.fields.invoice_number"),
dataIndex: ["Posting", "Reference"], dataIndex: ["Posting", "Reference"],
key: "reference", key: "reference",
}, },

View File

@@ -11,6 +11,7 @@ import {
Select, Select,
Space, Space,
Statistic, Statistic,
Switch,
Typography, Typography,
} from "antd"; } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
@@ -183,6 +184,13 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Space> <Space>
<DmsCdkMakes form={form} socket={socket} job={job} /> <DmsCdkMakes form={form} socket={socket} job={job} />
<DmsCdkMakesRefetch /> <DmsCdkMakesRefetch />
<Form.Item
name="dms_unsold"
label={t("jobs.fields.dms.dms_unsold")}
initialValue={false}
>
<Switch />
</Form.Item>
</Space> </Space>
</div> </div>
)} )}

View File

@@ -1,28 +1,28 @@
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons"; import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
import { import {
Button,
Divider, Divider,
Dropdown,
Form, Form,
Input, Input,
Menu,
Select, Select,
Space,
Tabs, Tabs,
Upload, Upload,
Space,
Menu,
Dropdown,
Button,
} from "antd"; } from "antd";
import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EmailDocumentsComponent from "../email-documents/email-documents.component";
import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia"; import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import { selectEmailConfig } from "../../redux/email/email.selectors"; import EmailDocumentsComponent from "../email-documents/email-documents.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -54,6 +54,15 @@ export function EmailOverlayComponent({
]), ]),
}); });
}; };
const handle_CC_Click = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({
cc: _.uniq([
...(form.getFieldValue("cc") || ""),
...(typeof email === "string" ? [email] : email),
]),
});
};
const menu = ( const menu = (
<div> <div>
@@ -74,6 +83,25 @@ export function EmailOverlayComponent({
</div> </div>
); );
const menuCC = (
<div>
<Menu onClick={handle_CC_Click}>
{bodyshop.employees
.filter((e) => e.user_email)
.map((e, idx) => (
<Menu.Item value={e.user_email} key={idx}>
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
return ( return (
<div> <div>
<Form.Item <Form.Item
@@ -122,7 +150,23 @@ export function EmailOverlayComponent({
> >
<Select mode="tags" tokenSeparators={[",", ";"]} /> <Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item> </Form.Item>
<Form.Item label={t("emails.fields.cc")} name="cc"> <Form.Item
label={
<Space>
{t("emails.fields.cc")}
<Dropdown overlay={menuCC}>
<a
className="ant-dropdown-link"
href=" #"
onClick={(e) => e.preventDefault()}
>
<UserAddOutlined />
</a>
</Dropdown>
</Space>
}
name="cc"
>
<Select mode="tags" tokenSeparators={[",", ";"]} /> <Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item

View File

@@ -0,0 +1,216 @@
import { AutoComplete, Divider, Input, Space } from "antd";
import axios from "axios";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearchOs() {
const { t } = useTranslation();
const history = useHistory();
const [loading, setLoading] = useState(false);
const [data, setData] = useState(false);
const executeSearch = async (v) => {
if (v && v && v !== "" && v.length >= 3) {
try {
setLoading(true);
const searchData = await axios.post("/search", {
search: v,
});
const resultsByType = {
payments: [],
jobs: [],
bills: [],
owners: [],
vehicles: [],
};
searchData.data.hits.hits.forEach((hit) => {
resultsByType[hit._index].push(hit._source);
});
setData([
{
label: renderTitle(t("menus.header.search.jobs")),
options: resultsByType.jobs.map((job) => {
return {
key: job.id,
value: job.ro_number || "N/A",
label: (
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<strong>{job.ro_number || t("general.labels.na")}</strong>
<span>{`${job.status || ""}`}</span>
<span>
<OwnerNameDisplay ownerObject={job} />
</span>
<span>{`${job.v_model_yr || ""} ${
job.v_make_desc || ""
} ${job.v_model_desc || ""}`}</span>
<span>{`${job.clm_no || ""}`}</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.owners")),
options: resultsByType.owners.map((owner) => {
return {
key: owner.id,
value: OwnerNameDisplayFunction(owner),
label: (
<Link to={`/manage/owners/${owner.id}`}>
<Space
size="small"
split={<Divider type="vertical" />}
wrap
>
<span>
<OwnerNameDisplay ownerObject={owner} />
</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.vehicles")),
options: resultsByType.vehicles.map((vehicle) => {
return {
key: vehicle.id,
value: `${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`,
label: (
<Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>
{`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span>
<VehicleVinDisplay>
{vehicle.v_vin || ""}
</VehicleVinDisplay>
</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.payments")),
options: resultsByType.payments.map((payment) => {
return {
key: payment.id,
value: `${payment.job?.ro_number} ${payment.amount}`,
label: (
<Link to={`/manage/jobs/${payment.job?.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{payment.paymentnum}</span>
<span>{payment.job?.ro_number}</span>
<span>{payment.memo || ""}</span>
<span>{payment.amount || ""}</span>
<span>{payment.transactionid || ""}</span>
</Space>
</Link>
),
};
}),
},
{
label: renderTitle(t("menus.header.search.bills")),
options: resultsByType.bills.map((bill) => {
return {
key: bill.id,
value: `${bill.invoice_number} - ${bill.vendor.name}`,
label: (
<Link to={`/manage/bills?billid=${bill.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{bill.invoice_number}</span>
<span>{bill.vendor.name}</span>
<span>{bill.date}</span>
</Space>
</Link>
),
};
}),
},
// {
// label: renderTitle(t("menus.header.search.phonebook")),
// options: resultsByType.search_phonebook.map((pb) => {
// return {
// key: pb.id,
// value: `${pb.firstname || ""} ${pb.lastname || ""} ${
// pb.company || ""
// }`,
// label: (
// <Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
// <Space size="small" split={<Divider type="vertical" />}>
// <span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
// pb.company || ""
// }`}</span>
// <PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
// <span>{pb.email}</span>
// </Space>
// </Link>
// ),
// };
// }),
// },
]);
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setLoading(false);
}
}
};
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => {
debouncedExecuteSearch(value);
};
const renderTitle = (title) => {
return <span>{title}</span>;
};
return (
<AutoComplete
options={data}
onSearch={handleSearch}
defaultActiveFirstOption
onSelect={(val, opt) => {
history.push(opt.label.props.to);
}}
onClear={() => setData([])}
>
<Input.Search
size="large"
placeholder={t("general.labels.globalsearch")}
enterButton
allowClear
loading={loading}
/>
</AutoComplete>
);
}

View File

@@ -8,7 +8,7 @@ import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, { import OwnerNameDisplay, {
OwnerNameDisplayFunction OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component"; } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() { export default function GlobalSearch() {
@@ -18,11 +18,18 @@ export default function GlobalSearch() {
useLazyQuery(GLOBAL_SEARCH_QUERY); useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => { const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v); if (
v &&
v.variables.search &&
v.variables.search !== "" &&
v.variables.search.length >= 3
)
callSearch(v);
}; };
const debouncedExecuteSearch = _.debounce(executeSearch, 750); const debouncedExecuteSearch = _.debounce(executeSearch, 750);
const handleSearch = (value) => { const handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } }); debouncedExecuteSearch({ variables: { search: value } });
}; };
@@ -37,7 +44,7 @@ export default function GlobalSearch() {
options: data.search_jobs.map((job) => { options: data.search_jobs.map((job) => {
return { return {
key: job.id, key: job.id,
value: job.ro_number, value: job.ro_number || "N/A",
label: ( label: (
<Link to={`/manage/jobs/${job.id}`}> <Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}> <Space size="small" split={<Divider type="vertical" />}>

View File

@@ -327,7 +327,9 @@ function Header({
icon={<SettingOutlined />} icon={<SettingOutlined />}
> >
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}> <Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link> <Link to="/manage/shop?tab=info">
{t("menus.header.shop_config")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}> <Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link> <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>

View File

@@ -3,9 +3,11 @@ import {
Button, Button,
Divider, Divider,
Dropdown, Dropdown,
Form,
Menu, Menu,
notification, notification,
Popover, Popover,
Select,
Space, Space,
} from "antd"; } from "antd";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
@@ -59,7 +61,10 @@ export function ScheduleEventComponent({
const blockContent = ( const blockContent = (
<div> <div>
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}> <Button
onClick={() => handleCancel({ id: event.id })}
disabled={event.arrived}
>
{t("appointments.actions.cancel")} {t("appointments.actions.cancel")}
</Button> </Button>
</div> </div>
@@ -203,10 +208,46 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button> <Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown> </Dropdown>
) : null} ) : null}
<Popover
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}> trigger="click"
{t("appointments.actions.cancel")} disabled={event.arrived}
</Button> content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
{event.isintake ? ( {event.isintake ? (
<Button <Button
disabled={event.arrived} disabled={event.arrived}
@@ -249,7 +290,7 @@ export function ScheduleEventComponent({
const RegularEvent = event.isintake ? ( const RegularEvent = event.isintake ? (
<Space <Space
wrap wrap
size='small' size="small"
style={{ style={{
backgroundColor: backgroundColor:
event.color && event.color.hex ? event.color.hex : event.color, event.color && event.color.hex ? event.color.hex : event.color,

View File

@@ -11,7 +11,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
const handleCancel = async (id) => { const handleCancel = async ({ id, lost_sale_reason }) => {
logImEXEvent("schedule_cancel_appt"); logImEXEvent("schedule_cancel_appt");
const cancelAppt = await cancelAppointment({ const cancelAppt = await cancelAppointment({
@@ -38,7 +38,8 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
job: { job: {
date_scheduled: null, date_scheduled: null,
scheduled_in: null, scheduled_in: null,
scheduled_completion:null, scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported, status: bodyshop.md_ro_statuses.default_imported,
}, },
}, },

View File

@@ -103,7 +103,12 @@ export function JobLinesComponent({
fixed: "left", fixed: "left",
key: "line_desc", key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({ className: record.manual_line && "job-line-manual" }), onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
},
}),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
@@ -343,7 +348,7 @@ export function JobLinesComponent({
onClick={() => { onClick={() => {
setJobLineEditContext({ setJobLineEditContext({
actions: { refetch: refetch, submit: form && form.submit }, actions: { refetch: refetch, submit: form && form.submit },
context: record, context: { ...record, jobid: job.id },
}); });
}} }}
> >

View File

@@ -289,7 +289,7 @@ export function JobLinesUpsertModalComponent({
name="prt_dsmk_p" name="prt_dsmk_p"
initialValue={0} initialValue={0}
> >
<InputNumber precision={0} min={0} max={100} /> <InputNumber precision={0} min={-100} max={100} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("joblines.fields.tax_part")} label={t("joblines.fields.tax_part")}

View File

@@ -13,8 +13,13 @@ import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component"; import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
import Axios from "axios"; import Axios from "axios";
import Dinero from "dinero.js";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal, jobLineEditModal: selectJobLineEditModal,
bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")), toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
@@ -23,7 +28,13 @@ const mapDispatchToProps = (dispatch) => ({
function JobLinesUpsertModalContainer({ function JobLinesUpsertModalContainer({
jobLineEditModal, jobLineEditModal,
toggleModalVisible, toggleModalVisible,
bodyshop,
}) { }) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { t } = useTranslation(); const { t } = useTranslation();
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE); const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
const [updateJobLine] = useMutation(UPDATE_JOB_LINE); const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
@@ -40,7 +51,15 @@ function JobLinesUpsertModalContainer({
manual_line: !( manual_line: !(
jobLineEditModal.context && jobLineEditModal.context.id jobLineEditModal.context && jobLineEditModal.context.id
), ),
...UndefinedToNull(values), ...UndefinedToNull({
...values,
prt_dsmk_m: Dinero({
amount: Math.round((values.act_price || 0) * 100),
})
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0),
}),
}, },
], ],
}, },
@@ -68,7 +87,15 @@ function JobLinesUpsertModalContainer({
const r = await updateJobLine({ const r = await updateJobLine({
variables: { variables: {
lineId: jobLineEditModal.context.id, lineId: jobLineEditModal.context.id,
line: values, line: {
...values,
prt_dsmk_m: Dinero({
amount: Math.round(values.act_price * 100),
})
.percentage(Math.abs(values.prt_dsmk_p || 0))
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
.toFormat(0.0),
},
}, },
refetchQueries: ["GET_LINE_TICKET_BY_PK"], refetchQueries: ["GET_LINE_TICKET_BY_PK"],
}); });
@@ -92,6 +119,9 @@ function JobLinesUpsertModalContainer({
} }
toggleModalVisible(); toggleModalVisible();
} }
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(jobLineEditModal.context.jobid);
}
setLoading(false); setLoading(false);
}; };

View File

@@ -115,7 +115,7 @@ export function JobPayments({
render: (text, record) => ( render: (text, record) => (
<Space wrap> <Space wrap>
<Button <Button
disabled={record.exportedat} // disabled={record.exportedat}
onClick={() => { onClick={() => {
setPaymentContext({ setPaymentContext({
actions: { refetch: refetch }, actions: { refetch: refetch },

View File

@@ -1,14 +1,14 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd"; import { Button, Form, notification } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -38,8 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true); setLoading(true);
const result = await updateJob({ const result = await updateJob({
variables: { jobId: job.id, job: values }, variables: { jobId: job.id, job: values },
refetchQueries: ['GET_JOB_BY_PK'], refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries:true awaitRefetchQueries: true,
}); });
const changedAuditFields = form.getFieldsValue( const changedAuditFields = form.getFieldsValue(
@@ -126,7 +126,10 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in"> <Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_repairstarted")} name="date_repairstarted"> <Form.Item
label={t("jobs.fields.date_repairstarted")}
name="date_repairstarted"
>
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -173,6 +176,9 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
> >
<DateTimePicker /> <DateTimePicker />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
</Form> </Form>

View File

@@ -1,19 +1,18 @@
import { useMutation } from "@apollo/client"; import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd"; import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import moment from "moment";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -150,6 +149,10 @@ export function JobAdminMarkReexport({
if (!result.errors) { if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
});
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.saving", { message: t("jobs.errors.saving", {

View File

@@ -33,8 +33,9 @@ export function JobsAdminUnvoid({
mutation UNVOID_JOB($jobId: uuid!) { mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${ update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported bodyshop.md_ro_statuses.default_imported
}"}) { }", date_void: null}) {
id id
date_void
voided voided
status status
} }

View File

@@ -3,8 +3,9 @@ import {
useApolloClient, useApolloClient,
useLazyQuery, useLazyQuery,
useMutation, useMutation,
useQuery, useQuery
} from "@apollo/client"; } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd"; import { Col, notification, Row } from "antd";
import Axios from "axios"; import Axios from "axios";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
@@ -19,7 +20,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
DELETE_AVAILABLE_JOB, DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_JOBS, QUERY_AVAILABLE_JOBS,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK, QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK
} from "../../graphql/available-jobs.queries"; } from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries"; import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries"; import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
@@ -27,10 +28,11 @@ import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm"; import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container"; import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
@@ -53,6 +55,11 @@ export function JobsAvailableContainer({
currentUser, currentUser,
insertAuditTrail, insertAuditTrail,
}) { }) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, { const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
@@ -155,6 +162,9 @@ export function JobsAvailableContainer({
}, },
}) })
.then((r) => { .then((r) => {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id);
}
notification["success"]({ notification["success"]({
message: t("jobs.successes.created"), message: t("jobs.successes.created"),
onClick: () => { onClick: () => {
@@ -241,7 +251,9 @@ export function JobsAvailableContainer({
}, },
}, },
}); });
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
}
if (updateResult.errors) { if (updateResult.errors) {
//error while inserting //error while inserting
notification["error"]({ notification["error"]({

View File

@@ -43,7 +43,7 @@ export function JobsConvertButton({
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const handleConvert = async ({ employee_csr, ...values }) => { const handleConvert = async ({ employee_csr, category, ...values }) => {
if (parentFormIsFieldsTouched()) { if (parentFormIsFieldsTouched()) {
alert(t("jobs.labels.savebeforeconversion")); alert(t("jobs.labels.savebeforeconversion"));
return; return;
@@ -55,6 +55,7 @@ export function JobsConvertButton({
job: { job: {
converted: true, converted: true,
...(bodyshop.enforce_conversion_csr ? { employee_csr } : {}), ...(bodyshop.enforce_conversion_csr ? { employee_csr } : {}),
...(bodyshop.enforce_conversion_category ? { category } : {}),
...values, ...values,
}, },
}, },
@@ -94,6 +95,7 @@ export function JobsConvertButton({
driveable: true, driveable: true,
towin: false, towin: false,
employee_csr: job.employee_csr, employee_csr: job.employee_csr,
category: job.category,
}} }}
> >
<Form.Item <Form.Item
@@ -107,8 +109,8 @@ export function JobsConvertButton({
]} ]}
> >
<Select> <Select>
{bodyshop.md_ins_cos.map((s) => ( {bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={s.name} value={s.name}> <Select.Option key={i} value={s.name}>
{s.name} {s.name}
</Select.Option> </Select.Option>
))} ))}
@@ -197,13 +199,35 @@ export function JobsConvertButton({
</Select> </Select>
</Form.Item> </Form.Item>
)} )}
<Form.Item {bodyshop.enforce_conversion_category && (
label={t("jobs.fields.ca_gst_registrant")} <Form.Item
name="ca_gst_registrant" name={"category"}
valuePropName="checked" label={t("jobs.fields.category")}
> rules={[
<Switch /> {
</Form.Item> required: bodyshop.enforce_conversion_category,
//message: t("general.validation.required"),
},
]}
>
<Select allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item
label={t("jobs.fields.ca_gst_registrant")}
name="ca_gst_registrant"
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
<Form.Item <Form.Item
label={t("jobs.fields.driveable")} label={t("jobs.fields.driveable")}
name="driveable" name="driveable"

View File

@@ -224,13 +224,15 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
> >
<CurrencyInput /> <CurrencyInput />
</Form.Item> </Form.Item>
<Form.Item {bodyshop.region_config.toLowerCase().startsWith("ca") && (
label={t("jobs.fields.ca_gst_registrant")} <Form.Item
name="ca_gst_registrant" label={t("jobs.fields.ca_gst_registrant")}
valuePropName="checked" name="ca_gst_registrant"
> valuePropName="checked"
<Switch /> >
</Form.Item> <Switch />
</Form.Item>
)}
<Form.Item <Form.Item
label={t("jobs.fields.other_amount_payable")} label={t("jobs.fields.other_amount_payable")}
name="other_amount_payable" name="other_amount_payable"

View File

@@ -9,7 +9,11 @@ const colSpan = {
lg: { span: 12 }, lg: { span: 12 },
}; };
export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) { export default function JobsCreateVehicleInfoComponent({
loading,
vehicles,
form,
}) {
const [state, setState] = useContext(JobCreateContext); const [state, setState] = useContext(JobCreateContext);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -58,7 +62,7 @@ export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) {
/> />
</Col> </Col>
<Col {...colSpan}> <Col {...colSpan}>
<JobsCreateVehicleInfoNewComponent /> <JobsCreateVehicleInfoNewComponent form={form}/>
</Col> </Col>
</Row> </Row>
</div> </div>

View File

@@ -20,6 +20,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
<JobsCreateVehicleInfoComponent <JobsCreateVehicleInfoComponent
loading={loading} loading={loading}
vehicles={data ? data.search_vehicles : null} vehicles={data ? data.search_vehicles : null}
form={form}
/> />
); );
} }

View File

@@ -4,8 +4,9 @@ import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
export default function JobsCreateVehicleInfoNewComponent() { export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext); const [state] = useContext(JobCreateContext);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -25,7 +26,7 @@ export default function JobsCreateVehicleInfoNewComponent() {
<Input disabled={!state.vehicle.new} /> <Input disabled={!state.vehicle.new} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow noDivider>
<Form.Item <Form.Item
label={t("vehicles.fields.v_color")} label={t("vehicles.fields.v_color")}
name={["vehicle", "data", "v_color"]} name={["vehicle", "data", "v_color"]}
@@ -52,8 +53,9 @@ export default function JobsCreateVehicleInfoNewComponent() {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow noDivider>
<Form.Item <Form.Item
span={10}
label={t("vehicles.fields.v_make_desc")} label={t("vehicles.fields.v_make_desc")}
name={["vehicle", "data", "v_make_desc"]} name={["vehicle", "data", "v_make_desc"]}
rules={[ rules={[
@@ -66,6 +68,7 @@ export default function JobsCreateVehicleInfoNewComponent() {
<Input disabled={!state.vehicle.new} /> <Input disabled={!state.vehicle.new} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
span={11}
label={t("vehicles.fields.v_model_desc")} label={t("vehicles.fields.v_model_desc")}
name={["vehicle", "data", "v_model_desc"]} name={["vehicle", "data", "v_model_desc"]}
rules={[ rules={[
@@ -77,6 +80,11 @@ export default function JobsCreateVehicleInfoNewComponent() {
> >
<Input disabled={!state.vehicle.new} /> <Input disabled={!state.vehicle.new} />
</Form.Item> </Form.Item>
<JobsCreateVehicleInfoPredefined
disabled={!state.vehicle.new}
form={form}
span={1}
/>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("vehicles.forms.registration")} grow> <LayoutFormRow header={t("vehicles.forms.registration")} grow>

View File

@@ -0,0 +1,81 @@
import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Popover, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import PredefinedVehicles from "./predefined-vehicles.js";
export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const { t } = useTranslation();
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
setSearch("");
};
const filteredPredefinedVehicles =
search === ""
? PredefinedVehicles
: PredefinedVehicles.filter(
(v) =>
v.make.toLowerCase().includes(search.toLowerCase()) ||
v.model.toLowerCase().includes(search.toLowerCase())
);
const popContent = () => (
<div>
<Table
size="small"
title={() => <Input.Search onSearch={(value) => setSearch(value)} />}
dataSource={filteredPredefinedVehicles}
columns={[
{
dataIndex: "make",
key: "make",
title: t("vehicles.fields.v_make_desc"),
},
{
dataIndex: "model",
key: "model",
title: t("vehicles.fields.v_model_desc"),
},
{
dataIndex: "select",
key: "select",
title: t("general.labels.actions"),
render: (value, record) => (
<Button
disabled={disabled}
onClick={() => {
form.setFieldsValue({
vehicle: {
data: {
v_make_desc: record.make,
v_model_desc: record.model,
},
},
});
setOpen(false);
setSearch("");
}}
>
<PlusOutlined />
</Button>
),
},
]}
/>
</div>
);
return (
<Popover
content={popContent}
trigger="click"
open={open}
placement="left"
onOpenChange={handleOpenChange}
destroyTooltipOnHide
>
<SearchOutlined style={{ cursor: "pointer" }} />
</Popover>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,10 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported"> <Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
<DateTimePicker disabled={true || jobRO} /> <DateTimePicker disabled={true || jobRO} />
</Form.Item> </Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
</FormRow> </FormRow>
</div> </div>
); );

View File

@@ -256,7 +256,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</FormRow> </FormRow>
<FormRow header={t("jobs.forms.other")}> <FormRow header={t("jobs.forms.other")}>
<Form.Item label={t("jobs.fields.category")} name="category"> <Form.Item label={t("jobs.fields.category")} name="category">
<Select disabled={jobRO}> <Select disabled={jobRO} allowClear>
{bodyshop.md_categories.map((s) => ( {bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}> <Select.Option key={s} value={s}>
{s} {s}
@@ -289,6 +289,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
> >
<Input disabled={jobRO} /> <Input disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item
label={t("jobs.fields.lost_sale_reason")}
name="lost_sale_reason"
>
<Input disabled={jobRO} allowClear />
</Form.Item>
</FormRow> </FormRow>
</div> </div>
); );

View File

@@ -1,6 +1,15 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd"; import {
Button,
Dropdown,
Form,
Menu,
Popconfirm,
Popover,
Select,
notification,
} from "antd";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -15,6 +24,7 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent"; import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component"; import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
@@ -130,35 +140,63 @@ export function JobsDetailHeaderActions({
<Menu.Item <Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled} disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
> >
<Popconfirm <Popover
title={t("general.labels.areyousure")} trigger="click"
okText="Yes"
cancelText="No"
onClick={(e) => e.stopPropagation()}
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled} disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
onConfirm={async () => { content={
const jobUpdate = await cancelAllAppointments({ <Form
variables: { layout="vertical"
jobid: job.id, onFinish={async ({ lost_sale_reason }) => {
job: { const jobUpdate = await cancelAllAppointments({
date_scheduled: null, variables: {
scheduled_in: null, jobid: job.id,
scheduled_completion: null, job: {
status: bodyshop.md_ro_statuses.default_imported, date_scheduled: null,
}, scheduled_in: null,
}, scheduled_completion: null,
}); lost_sale_reason,
if (!jobUpdate.errors) { status: bodyshop.md_ro_statuses.default_imported,
notification["success"]({ },
message: t("appointments.successes.canceled"), },
}); });
return; if (!jobUpdate.errors) {
} notification["success"]({
}} message: t("appointments.successes.canceled"),
getPopupContainer={(trigger) => trigger.parentNode} });
return;
}
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button
htmlType="submit"
disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
> >
{t("menus.jobsactions.cancelallappointments")} {t("menus.jobsactions.cancelallappointments")}
</Popconfirm> </Popover>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
disabled={ disabled={
@@ -439,54 +477,57 @@ export function JobsDetailHeaderActions({
)} )}
<JobsDetailHeaderActionsAddevent jobid={job.id} /> <JobsDetailHeaderActionsAddevent jobid={job.id} />
{!jobRO && job.converted && ( {!jobRO && job.converted && (
<Menu.Item> <RbacWrapper action="jobs:void" noauth>
<Popconfirm <Menu.Item>
title={t("jobs.labels.voidjob")} <Popconfirm
okText="Yes" title={t("jobs.labels.voidjob")}
cancelText="No" okText="Yes"
onClick={(e) => e.stopPropagation()} cancelText="No"
onConfirm={async () => { onClick={(e) => e.stopPropagation()}
//delete the job. onConfirm={async () => {
const result = await voidJob({ //delete the job.
variables: { const result = await voidJob({
jobId: job.id, variables: {
job: { jobId: job.id,
status: bodyshop.md_ro_statuses.default_void, job: {
voided: true, status: bodyshop.md_ro_statuses.default_void,
scheduled_in: null, voided: true,
scheduled_completion: null, scheduled_in: null,
inproduction: false, scheduled_completion: null,
}, inproduction: false,
note: [ date_void: new Date(),
{
jobid: job.id,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.voidnote"),
}, },
], note: [
}, {
}); jobid: job.id,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.voidnote"),
},
],
},
});
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ notification["success"]({
message: t("jobs.successes.voided"), message: t("jobs.successes.voided"),
}); });
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.voiding", { message: t("jobs.errors.voiding", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors),
}), }),
}); });
} }
}} }}
getPopupContainer={(trigger) => trigger.parentNode} getPopupContainer={(trigger) => trigger.parentNode}
> >
{t("menus.jobsactions.void")} {t("menus.jobsactions.void")}
</Popconfirm> </Popconfirm>
</Menu.Item> </Menu.Item>
</RbacWrapper>
)} )}
</Menu> </Menu>
); );

View File

@@ -40,24 +40,26 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
> >
<CurrencyInput disabled={jobRO} min={0} /> <CurrencyInput disabled={jobRO} min={0} />
</Form.Item> </Form.Item>
<Tooltip title={t("jobs.labels.ca_gst_all_if_null")}> {bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item <Tooltip title={t("jobs.labels.ca_gst_all_if_null")}>
label={t("jobs.fields.ca_customer_gst")} <Form.Item
name="ca_customer_gst" label={t("jobs.fields.ca_customer_gst")}
> name="ca_customer_gst"
<CurrencyInput >
disabled={jobRO} <CurrencyInput
min={0} disabled={jobRO}
max={ min={0}
Math.round( max={
(job.job_totals && Math.round(
job.job_totals.totals.federal_tax.amount) || (job.job_totals &&
0 job.job_totals.totals.federal_tax.amount) ||
) / 100 0
} ) / 100
/> }
</Form.Item> />
</Tooltip> </Form.Item>
</Tooltip>
)}
<Form.Item <Form.Item
label={t("jobs.fields.other_amount_payable")} label={t("jobs.fields.other_amount_payable")}
name="other_amount_payable" name="other_amount_payable"
@@ -82,12 +84,14 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
> >
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} /> <CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
</Form.Item> </Form.Item>
<Space align="end"> {bodyshop.region_config === "CA_BC" && (
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt"> <Space align="center">
<CurrencyInput disabled={jobRO} min={0} /> <Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">
</Form.Item> <CurrencyInput disabled={jobRO} min={0} />
<CABCpvrtCalculator form={form} disabled={jobRO} /> </Form.Item>
</Space> <CABCpvrtCalculator form={form} disabled={jobRO} />
</Space>
)}
<Form.Item <Form.Item
label={t("jobs.fields.auto_add_ats")} label={t("jobs.fields.auto_add_ats")}
name="auto_add_ats" name="auto_add_ats"
@@ -141,13 +145,15 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
> >
<InputNumber min={0} max={1} precision={2} disabled={jobRO} /> <InputNumber min={0} max={1} precision={2} disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item {bodyshop.region_config.toLowerCase().startsWith("ca") && (
label={t("jobs.fields.ca_gst_registrant")} <Form.Item
name="ca_gst_registrant" label={t("jobs.fields.ca_gst_registrant")}
valuePropName="checked" name="ca_gst_registrant"
> valuePropName="checked"
<Switch disabled={jobRO} /> >
</Form.Item> <Switch disabled={jobRO} />
</Form.Item>
)}
</FormRow> </FormRow>
<Divider <Divider
orientation="left" orientation="left"

View File

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios"; import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes"; import formatBytes from "../../utils/formatbytes";
import yauzl from "yauzl"; //import yauzl from "yauzl";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -69,44 +69,44 @@ export function JobsDocumentsDownloadButton({
setDownload(null); setDownload(null);
if (Direct_Media_Download.treatment === "on") { if (Direct_Media_Download.treatment === "on") {
try { try {
const parentDir = await window.showDirectoryPicker({ // const parentDir = await window.showDirectoryPicker({
id: "media", // id: "media",
startIn: "downloads", // startIn: "downloads",
}); // });
const directory = await parentDir.getDirectoryHandle(identifier, { // const directory = await parentDir.getDirectoryHandle(identifier, {
create: true, // create: true,
}); // });
yauzl.fromBuffer( // yauzl.fromBuffer(
Buffer.from(theDownloadedZip.data), // Buffer.from(theDownloadedZip.data),
{}, // {},
(err, zipFile) => { // (err, zipFile) => {
if (err) throw err; // if (err) throw err;
zipFile.on("entry", (entry) => { // zipFile.on("entry", (entry) => {
zipFile.openReadStream(entry, async (readErr, readStream) => { // zipFile.openReadStream(entry, async (readErr, readStream) => {
if (readErr) { // if (readErr) {
zipFile.close(); // zipFile.close();
throw readErr; // throw readErr;
} // }
if (err) throw err; // if (err) throw err;
let fileSystemHandle = await directory.getFileHandle( // let fileSystemHandle = await directory.getFileHandle(
entry.fileName, // entry.fileName,
{ // {
create: true, // create: true,
} // }
); // );
const writable = await fileSystemHandle.createWritable(); // const writable = await fileSystemHandle.createWritable();
readStream.on("data", async function (chunk) { // readStream.on("data", async function (chunk) {
await writable.write(chunk); // await writable.write(chunk);
}); // });
readStream.on("end", async function () { // readStream.on("end", async function () {
await writable.close(); // await writable.close();
}); // });
}); // });
}); // });
} // }
); // );
} catch (e) { } catch (e) {
console.log(e); console.log(e);
standardMediaDownload(theDownloadedZip.data); standardMediaDownload(theDownloadedZip.data);

View File

@@ -1,7 +1,7 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd"; import { Button, Card, Col, Row, Space } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery"; import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DocumentsUploadComponent from "../documents-upload/documents-upload.component"; import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
import { DetermineFileType } from "../documents-upload/documents-upload.utility"; import { DetermineFileType } from "../documents-upload/documents-upload.utility";
@@ -11,6 +11,9 @@ import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.compo
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component"; import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component"; import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
function JobsDocumentsComponent({ function JobsDocumentsComponent({
data, data,
jobId, jobId,
@@ -23,11 +26,7 @@ function JobsDocumentsComponent({
}) { }) {
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] }); const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation(); const { t } = useTranslation();
const [index, setIndex] = useState(0); const [modalState, setModalState] = useState({ open: false, index: 0 });
const onCurrentImageChange = (index) => {
setIndex(index);
};
useEffect(() => { useEffect(() => {
let documents = data.reduce( let documents = data.reduce(
@@ -35,14 +34,16 @@ function JobsDocumentsComponent({
const fileType = DetermineFileType(value.type); const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) { if (value.type.startsWith("image")) {
acc.images.push({ acc.images.push({
src: GenerateSrcUrl(value), // src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value), src: GenerateThumbUrl(value),
thumbnailHeight: 225, // src: GenerateSrcUrl(value),
thumbnailWidth: 225, // thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value),
height: 225,
width: 225,
isSelected: false, isSelected: false,
key: value.key, key: value.key,
extension: value.extension, extension: value.extension,
id: value.id, id: value.id,
type: value.type, type: value.type,
size: value.size, size: value.size,
@@ -62,7 +63,7 @@ function JobsDocumentsComponent({
const fileName = value.key.split("/").pop(); const fileName = value.key.split("/").pop();
acc.other.push({ acc.other.push({
source: GenerateSrcUrl(value), source: GenerateSrcUrl(value),
src: "", src: thumb,
thumbnail: thumb, thumbnail: thumb,
tags: [ tags: [
{ {
@@ -85,10 +86,9 @@ function JobsDocumentsComponent({
] ]
: []), : []),
], ],
thumbnailHeight: 225, height: 225,
thumbnailWidth: 225, width: 225,
isSelected: false, isSelected: false,
extension: value.extension, extension: value.extension,
key: value.key, key: value.key,
id: value.id, id: value.id,
@@ -148,35 +148,15 @@ function JobsDocumentsComponent({
<Card title={t("jobs.labels.documents-images")}> <Card title={t("jobs.labels.documents-images")}>
<Gallery <Gallery
images={galleryImages.images} images={galleryImages.images}
backdropClosesModal={true} onClick={(index, item) => {
currentImageWillChange={onCurrentImageChange} setModalState({ open: true, index: index });
customControls={[ // window.open(
<Button // item.fullsize,
key="edit-button" // "_blank",
style={{ // "toolbar=0,location=0,menubar=0"
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,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}} }}
onSelectImage={(index, image) => { onSelect={(index, image) => {
setgalleryImages({ setgalleryImages({
...galleryImages, ...galleryImages,
images: galleryImages.images.map((g, idx) => images: galleryImages.images.map((g, idx) =>
@@ -191,8 +171,6 @@ function JobsDocumentsComponent({
<Card title={t("jobs.labels.documents-other")}> <Card title={t("jobs.labels.documents-other")}>
<Gallery <Gallery
images={galleryImages.other} images={galleryImages.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => { thumbnailStyle={() => {
return { return {
backgroundImage: <FileExcelFilled />, backgroundImage: <FileExcelFilled />,
@@ -201,14 +179,14 @@ function JobsDocumentsComponent({
cursor: "pointer", cursor: "pointer",
}; };
}} }}
onClickThumbnail={(index) => { onClick={(index) => {
window.open( window.open(
galleryImages.other[index].source, galleryImages.other[index].source,
"_blank", "_blank",
"toolbar=0,location=0,menubar=0" "toolbar=0,location=0,menubar=0"
); );
}} }}
onSelectImage={(index) => { onSelect={(index) => {
setgalleryImages({ setgalleryImages({
...galleryImages, ...galleryImages,
other: galleryImages.other.map((g, idx) => other: galleryImages.other.map((g, idx) =>
@@ -219,6 +197,53 @@ function JobsDocumentsComponent({
/> />
</Card> </Card>
</Col> </Col>
{modalState.open && (
<Lightbox
toolbarButtons={[
<EditFilled
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${
window.location.host
}/edit?documentId=${
galleryImages.images[modalState.index].id
}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
/>,
]}
mainSrc={galleryImages.images[modalState.index].fullsize}
nextSrc={
galleryImages.images[
(modalState.index + 1) % galleryImages.images.length
].fullsize
}
prevSrc={
galleryImages.images[
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length
].fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % galleryImages.images.length,
})
}
/>
)}
</Row> </Row>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import Gallery from "react-grid-gallery"; import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility"; import { GenerateThumbUrl } from "./job-documents.utility";
function JobsDocumentGalleryExternal({ function JobsDocumentGalleryExternal({
data, data,
@@ -15,8 +15,8 @@ function JobsDocumentGalleryExternal({
let documents = data.reduce((acc, value) => { let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) { if (value.type.startsWith("image")) {
acc.push({ acc.push({
src: GenerateSrcUrl(value), //src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value), src: GenerateThumbUrl(value),
thumbnailHeight: 225, thumbnailHeight: 225,
thumbnailWidth: 225, thumbnailWidth: 225,
isSelected: false, isSelected: false,
@@ -39,7 +39,7 @@ function JobsDocumentGalleryExternal({
<Gallery <Gallery
images={galleryImages} images={galleryImages}
backdropClosesModal={true} backdropClosesModal={true}
onSelectImage={(index, image) => { onSelect={(index, image) => {
setgalleryImages( setgalleryImages(
galleryImages.map((g, idx) => galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g index === idx ? { ...g, isSelected: !g.isSelected } : g

View File

@@ -1,7 +1,7 @@
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons"; import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { Alert, Button, Card, Space } from "antd"; import { Alert, Button, Card, Space } from "antd";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery"; import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -19,6 +19,9 @@ import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.downl
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component"; import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component"; import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
allMedia: selectAllMedia, allMedia: selectAllMedia,
@@ -49,6 +52,7 @@ export function JobsDocumentsLocalGallery({
vendorid, vendorid,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalState, setModalState] = useState({ open: false, index: 0 });
useEffect(() => { useEffect(() => {
if (job) { if (job) {
if (invoice_number) { if (invoice_number) {
@@ -70,12 +74,20 @@ export function JobsDocumentsLocalGallery({
) { ) {
acc.images.push({ acc.images.push({
...val, ...val,
fullsize: val.src,
src: val.thumbnail,
height: val.thumbnailHeight,
width: val.thumbnailWidth,
...(val.optimized && { src: val.optimized, fullsize: val.src }), ...(val.optimized && { src: val.optimized, fullsize: val.src }),
}); });
if (val.optimized) optimized = true; if (val.optimized) optimized = true;
} else { } else {
acc.other.push({ acc.other.push({
...val, ...val,
fullsize: val.src,
src: val.thumbnail,
height: val.thumbnailHeight,
width: val.thumbnailWidth,
tags: [{ value: val.filename, title: val.filename }], tags: [{ value: val.filename, title: val.filename }],
}); });
} }
@@ -120,8 +132,7 @@ export function JobsDocumentsLocalGallery({
<Card title={t("jobs.labels.documents-images")}> <Card title={t("jobs.labels.documents-images")}>
<Gallery <Gallery
images={jobMedia.images} images={jobMedia.images}
backdropClosesModal={true} onSelect={(index, image) => {
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename }); toggleMediaSelected({ jobid: job.id, filename: image.filename });
}} }}
{...(optimized && { {...(optimized && {
@@ -133,24 +144,23 @@ export function JobsDocumentsLocalGallery({
/>, />,
], ],
})} })}
onClickImage={(props) => { onClick={(index) => {
const media = allMedia[job.id].find( setModalState({ open: true, index: index });
(m) => m.optimized === props.target.src // const media = allMedia[job.id].find(
); // (m) => m.optimized === item.src
// );
window.open( // window.open(
media ? media.src : props.target.src, // media ? media.fullsize : item.fullsize,
"_blank", // "_blank",
"toolbar=0,location=0,menubar=0" // "toolbar=0,location=0,menubar=0"
); // );
}} }}
/> />
</Card> </Card>
<Card title={t("jobs.labels.documents-other")}> <Card title={t("jobs.labels.documents-other")}>
<Gallery <Gallery
images={jobMedia.other} images={jobMedia.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => { thumbnailStyle={() => {
return { return {
backgroundImage: <FileExcelFilled />, backgroundImage: <FileExcelFilled />,
@@ -159,18 +169,48 @@ export function JobsDocumentsLocalGallery({
cursor: "pointer", cursor: "pointer",
}; };
}} }}
onClickThumbnail={(index) => { onClick={(index) => {
window.open( window.open(
jobMedia.other[index].src, jobMedia.other[index].fullsize,
"_blank", "_blank",
"toolbar=0,location=0,menubar=0" "toolbar=0,location=0,menubar=0"
); );
}} }}
onSelectImage={(index, image) => { onSelect={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename }); toggleMediaSelected({ jobid: job.id, filename: image.filename });
}} }}
/> />
</Card> </Card>
{modalState.open && (
<Lightbox
mainSrc={jobMedia.images[modalState.index].fullsize}
nextSrc={
jobMedia.images[(modalState.index + 1) % jobMedia.images.length]
.fullsize
}
prevSrc={
jobMedia.images[
(modalState.index + jobMedia.images.length - 1) %
jobMedia.images.length
].fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + jobMedia.images.length - 1) %
jobMedia.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % jobMedia.images.length,
})
}
/>
)}
</div> </div>
); );
} }

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import Gallery from "react-grid-gallery"; import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -38,7 +38,7 @@ function JobDocumentsLocalGalleryExternal({
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if ( jobId) { if (jobId) {
getJobMedia(jobId); getJobMedia(jobId);
} }
}, [jobId, getJobMedia]); }, [jobId, getJobMedia]);
@@ -52,11 +52,15 @@ function JobDocumentsLocalGalleryExternal({
val.type.mime && val.type.mime &&
val.type.mime.startsWith("image") val.type.mime.startsWith("image")
) { ) {
acc.push(val); acc.push({ ...val, src: val.thumbnail });
} }
return acc; return acc;
}, []) }, [])
: []; : [];
console.log(
"🚀 ~ file: jobs-documents-local-gallery.external.component.jsx:48 ~ useEffect ~ documents:",
documents
);
setgalleryImages(documents); setgalleryImages(documents);
}, [allMedia, jobId, setgalleryImages, t]); }, [allMedia, jobId, setgalleryImages, t]);
@@ -65,8 +69,7 @@ function JobDocumentsLocalGalleryExternal({
<div className="clearfix"> <div className="clearfix">
<Gallery <Gallery
images={galleryImages} images={galleryImages}
backdropClosesModal={true} onSelect={(index, image) => {
onSelectImage={(index, image) => {
setgalleryImages( setgalleryImages(
galleryImages.map((g, idx) => galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g index === idx ? { ...g, isSelected: !g.isSelected } : g

View File

@@ -1,8 +1,9 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
@@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsList({ bodyshop, refetch, loading, jobs, total }) { export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const history = useHistory(); const history = useHistory();
@@ -193,6 +196,28 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchJobs();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchJobs(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "jobs",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return ( return (
<Card <Card
extra={ extra={
@@ -205,6 +230,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
@@ -220,24 +246,32 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
searchJobs(value);
}} }}
loading={loading || searchLoading}
enterButton enterButton
/> />
</Space> </Space>
} }
> >
<Table <Table
loading={loading} loading={loading || searchLoading}
pagination={{ pagination={
position: "top", search?.search
pageSize: 25, ? {
current: parseInt(page || 1), pageSize: 25,
total: total, showSizeChanger: false,
showSizeChanger: false, }
}} : {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={jobs} dataSource={search?.search ? openSearchResults : jobs}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -112,7 +112,9 @@ export function JobsList({ bodyshop }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sorter: (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) -
parseInt((b.ro_number || "0").replace(/\D/g, "")),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
@@ -260,6 +262,19 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"], responsive: ["md"],
}, },
{ {

View File

@@ -75,6 +75,27 @@ export function JobNotesComponent({
</span> </span>
), ),
}, },
{
title: t("notes.fields.type"),
dataIndex: "type",
key: "type",
width: 120,
filteredValue: filter?.type || null,
filters: [
{ value: "general", text: t("notes.fields.types.general") },
{ value: "customer", text: t("notes.fields.types.customer") },
{ value: "shop", text: t("notes.fields.types.shop") },
{ value: "office", text: t("notes.fields.types.office") },
{ value: "parts", text: t("notes.fields.types.parts") },
{ value: "paint", text: t("notes.fields.types.paint") },
{
value: "supplement",
text: t("notes.fields.types.supplement"),
},
],
onFilter: (value, record) => value.includes(record.type),
render: (text, record) => t(`notes.fields.types.${record.type}`),
},
{ {
title: t("notes.fields.text"), title: t("notes.fields.text"),
dataIndex: "text", dataIndex: "text",
@@ -106,7 +127,7 @@ export function JobNotesComponent({
title: t("notes.actions.actions"), title: t("notes.actions.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
width: 150, width: 200,
render: (text, record) => ( render: (text, record) => (
<Space wrap> <Space wrap>
<Button <Button

View File

@@ -272,6 +272,19 @@ export function JobsReadyList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
responsive: ["md"], responsive: ["md"],
}, },
{ {

View File

@@ -207,7 +207,7 @@ export function LaborAllocationsTable({
<Card title={t("jobs.labels.laborallocations")}> <Card title={t("jobs.labels.laborallocations")}>
<Table <Table
columns={columns} columns={columns}
rowKey="cost_center" rowKey={(record) => `${record.cost_center} ${record.mod_lbr_ty}`}
pagination={false} pagination={false}
onChange={handleTableChange} onChange={handleTableChange}
dataSource={totals} dataSource={totals}

View File

@@ -6,10 +6,6 @@ export const CalculateAllocationsTotals = (
timetickets, timetickets,
adjustments = [] adjustments = []
) => { ) => {
console.log(
"🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments",
adjustments
);
const responsibilitycenters = bodyshop.md_responsibility_centers; const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty); const jobCodes = joblines.map((item) => item.mod_lbr_ty);
//.filter((value, index, self) => self.indexOf(value) === index && !!value); //.filter((value, index, self) => self.indexOf(value) === index && !!value);

View File

@@ -1,4 +1,14 @@
import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } from "antd"; import {
Checkbox,
Col,
Form,
Input,
Row,
Select,
Space,
Switch,
Tag,
} from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -46,6 +56,28 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
<Switch /> <Switch />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}>
<Form.Item
label={t("notes.fields.type")}
name="type"
initialValue="general"
>
<Select
options={[
{ value: "general", label: t("notes.fields.types.general") },
{ value: "customer", label: t("notes.fields.types.customer") },
{ value: "shop", label: t("notes.fields.types.shop") },
{ value: "office", label: t("notes.fields.types.office") },
{ value: "parts", label: t("notes.fields.types.parts") },
{ value: "paint", label: t("notes.fields.types.paint") },
{
value: "supplement",
label: t("notes.fields.types.supplement"),
},
]}
/>
</Form.Item>
</Col>
<Col span={8}> <Col span={8}>
<NotesPresetButton form={form} /> <NotesPresetButton form={form} />
</Col> </Col>

View File

@@ -34,7 +34,7 @@ export function NoteUpsertModalContainer({
const [updateNote] = useMutation(UPDATE_NOTE); const [updateNote] = useMutation(UPDATE_NOTE);
const { visible, context, actions } = noteUpsertModal; const { visible, context, actions } = noteUpsertModal;
const { jobId, existingNote } = context; const { jobId, existingNote, text } = context;
const { refetch } = actions; const { refetch } = actions;
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -45,8 +45,12 @@ export function NoteUpsertModalContainer({
form.setFieldsValue(existingNote); form.setFieldsValue(existingNote);
} else if (!existingNote && visible) { } else if (!existingNote && visible) {
form.resetFields(); form.resetFields();
if (text) {
form.setFieldValue("text", text);
}
} }
}, [existingNote, form, visible]); }, [existingNote, form, visible, text]);
const handleFinish = async (formValues) => { const handleFinish = async (formValues) => {
const { relatedros, ...values } = formValues; const { relatedros, ...values } = formValues;
@@ -82,6 +86,7 @@ export function NoteUpsertModalContainer({
{ ...values, jobid: jobId, created_by: currentUser.email }, { ...values, jobid: jobId, created_by: currentUser.email },
], ],
}, },
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"],
}); });
if (AdditionalNoteInserts.length > 0) { if (AdditionalNoteInserts.length > 0) {

View File

@@ -1,15 +1,40 @@
import { Button, Form, notification, PageHeader } from "antd"; import { Button, Form, notification, PageHeader, Popconfirm } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_OWNER } from "../../graphql/owners.queries"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
import OwnerDetailFormComponent from "./owner-detail-form.component"; import OwnerDetailFormComponent from "./owner-detail-form.component";
function OwnerDetailFormContainer({ owner, refetch }) { function OwnerDetailFormContainer({ owner, refetch }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const history = useHistory();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [updateOwner] = useMutation(UPDATE_OWNER); const [updateOwner] = useMutation(UPDATE_OWNER);
const [deleteOwner] = useMutation(DELETE_OWNER);
const handleDelete = async () => {
setLoading(true);
const result = await deleteOwner({
variables: { id: owner.id },
});
console.log(result);
if (result.errors) {
notification["error"]({
message: t("owners.errors.deleting", {
error: JSON.stringify(result.errors),
}),
});
setLoading(false);
} else {
notification["success"]({
message: t("owners.successes.delete"),
});
setLoading(false);
history.push(`/manage/owners`);
}
};
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
@@ -41,15 +66,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
<> <>
<PageHeader <PageHeader
title={t("menus.header.owners")} title={t("menus.header.owners")}
extra={ extra={[
<Popconfirm
trigger="click"
onConfirm={handleDelete}
disabled={owner.jobs.length !== 0}
title={t("owners.labels.deleteconfirm")}
>
<Button
type="danger"
loading={loading}
disabled={owner.jobs.length !== 0}
>
{t("general.actions.delete")}
</Button>
</Popconfirm>,
<Button <Button
type="primary" type="primary"
loading={loading} loading={loading}
onClick={() => form.submit()} onClick={() => form.submit()}
> >
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>,
} ]}
/> />
<Form <Form
form={form} form={form}

View File

@@ -201,6 +201,7 @@ export function PartsOrderListTableComponent({
subject: record.return subject: record.return
? Templates.parts_return_slip.subject ? Templates.parts_return_slip.subject
: Templates.parts_order.subject, : Templates.parts_order.subject,
to: record.vendor.email,
}} }}
id={job.id} id={job.id}
/> />
@@ -296,7 +297,6 @@ export function PartsOrderListTableComponent({
sortOrder: sortOrder:
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order, state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
}, },
{ {
title: t("parts_orders.fields.act_price"), title: t("parts_orders.fields.act_price"),
dataIndex: "act_price", dataIndex: "act_price",

View File

@@ -0,0 +1,100 @@
import React from "react";
import { Button, notification } from "antd";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const PaymentMarkForExportButton = ({
bodyshop,
payment,
refetch,
setPaymentContext,
currentUser,
}) => {
const { t } = useTranslation();
const [insertExportLog, { loading: exportLogLoading }] =
useMutation(INSERT_EXPORT_LOG);
const [updatePayment, { loading: updatePaymentLoading }] =
useMutation(UPDATE_PAYMENT);
const handleClick = async () => {
const today = new Date();
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: payment.id,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentId: payment.id,
payment: {
exportedat: today,
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessmarkforexport",
message: t("payments.successes.markexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...payment,
exportedat: today,
},
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
};
return (
<Button
onClick={handleClick}
loading={exportLogLoading || updatePaymentLoading}
disabled={!!payment.exportedat}
>
{t("payments.labels.markexported")}
</Button>
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PaymentMarkForExportButton);

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Form, Modal, notification } from "antd"; import { Button, Form, Modal, notification, Space } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -19,6 +19,8 @@ import {
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import PaymentForm from "../payment-form/payment-form.component"; import PaymentForm from "../payment-form/payment-form.component";
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
paymentModal: selectPayment, paymentModal: selectPayment,
@@ -52,7 +54,7 @@ function PaymentModalContainer({
const { useStripe, sendby, ...paymentObj } = values; const { useStripe, sendby, ...paymentObj } = values;
setLoading(true); setLoading(true);
let updatedPayment; //Moved up from if statement for greater scope.
try { try {
if (!context || (context && !context.id)) { if (!context || (context && !context.id)) {
const newPayment = await insertPayment({ const newPayment = await insertPayment({
@@ -87,7 +89,7 @@ function PaymentModalContainer({
); );
} }
} else { } else {
const updatedPayment = await updatePayment({ updatedPayment = await updatePayment({
variables: { variables: {
paymentId: context.id, paymentId: context.id,
payment: paymentObj, payment: paymentObj,
@@ -101,7 +103,11 @@ function PaymentModalContainer({
} }
} }
if (actions.refetch) actions.refetch(); if (actions.refetch)
actions.refetch(
updatedPayment && updatedPayment.data.update_payments.returning[0]
);
if (enterAgain) { if (enterAgain) {
const prev = form.getFieldsValue(["date"]); const prev = form.getFieldsValue(["date"]);
@@ -124,7 +130,11 @@ function PaymentModalContainer({
}; };
useEffect(() => { useEffect(() => {
if (visible) form.resetFields(); if (visible) {
form.resetFields();
form.resetFields();
form.setFieldsValue(context);
}
}, [visible, form, context]); }, [visible, form, context]);
useEffect(() => { useEffect(() => {
@@ -139,6 +149,7 @@ function PaymentModalContainer({
: t("payments.labels.edit") : t("payments.labels.edit")
} }
visible={visible} visible={visible}
destroyOnClose
okText={t("general.actions.save")} okText={t("general.actions.save")}
onOk={() => form.submit()} onOk={() => form.submit()}
width="50%" width="50%"
@@ -167,12 +178,24 @@ function PaymentModalContainer({
</span> </span>
} }
> >
{!context || (context && !context.id) ? null : (
<Space>
<PaymentReexportButton payment={context} refetch={actions.refetch} />
<PaymentMarkForExportButton
bodyshop={bodyshop}
payment={context}
refetch={actions.refetch}
/>
</Space>
)}
<Form <Form
onFinish={handleFinish} onFinish={handleFinish}
autoComplete={"off"} autoComplete={"off"}
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={context || {}} initialValues={context || {}}
disabled={context?.exportedat}
> >
<PaymentForm form={form} /> <PaymentForm form={form} />
</Form> </Form>

View File

@@ -0,0 +1,66 @@
import React from "react";
import { Button, notification } from "antd";
import { useTranslation } from "react-i18next";
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
import { useMutation } from "@apollo/client";
import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
const mapDispatchToProps = (dispatch) => ({
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => {
const { t } = useTranslation();
const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT);
const handleClick = async () => {
const paymentUpdateResponse = await updatePayment({
variables: {
paymentId: payment.id,
payment: {
exportedat: null,
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.markreexported"),
});
if (refetch) refetch();
setPaymentContext({
actions: {
refetch,
},
context: {
...payment,
exportedat: null,
},
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
};
return (
<Button
onClick={handleClick}
loading={loading}
disabled={!payment.exportedat}
>
{t("payments.labels.markforreexport")}
</Button>
);
};
export default connect(null, mapDispatchToProps)(PaymentReexportButton);

View File

@@ -1,20 +1,23 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { Button, Card, Input, Space, Table, Typography } from "antd"; import { Button, Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_PAYMENT_BY_ID } from "../../graphql/payments.queries";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container"; import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -39,7 +42,10 @@ export function PaymentsListPaginated({
bodyshop, bodyshop,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const client = useApolloClient();
const history = useHistory(); const history = useHistory();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
@@ -52,13 +58,17 @@ export function PaymentsListPaginated({
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), // sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder, // sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => ( render: (text, record) => {
<Link to={"/manage/jobs/" + record.job.id}> return record.job ? (
{record.job.ro_number || t("general.labels.na")} <Link to={"/manage/jobs/" + record.job.id}>
</Link> {record.job.ro_number || t("general.labels.na")}
), </Link>
) : (
<span>{t("general.labels.na")}</span>
);
},
}, },
{ {
title: t("payments.fields.paymentnum"), title: t("payments.fields.paymentnum"),
@@ -72,16 +82,16 @@ export function PaymentsListPaginated({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), // sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sortOrder: sortcolumn === "owner" && sortorder, // sortOrder: sortcolumn === "owner" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.job.owner ? ( return record.job?.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}> <Link to={"/manage/owners/" + record.job?.owner?.id}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record.job} />
</Link> </Link>
) : ( ) : (
<span> <span>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record.job} />
</span> </span>
); );
}, },
@@ -146,11 +156,34 @@ export function PaymentsListPaginated({
render: (text, record) => ( render: (text, record) => (
<Space> <Space>
<Button <Button
disabled={record.exportedat} // disabled={record.exportedat}
onClick={() => { onClick={async () => {
let apolloResults;
if (search.search) {
const { data } = await client.query({
query: QUERY_PAYMENT_BY_ID,
variables: {
paymentId: record.id,
},
});
apolloResults = data.payments_by_pk;
}
setPaymentContext({ setPaymentContext({
actions: { refetch: refetch }, actions: {
context: record, refetch: apolloResults
? (updatedRecord) => {
setOpenSearchResults((results) =>
results.map((result) => {
if (result.id !== record.id) {
return result;
}
return updatedRecord;
})
);
}
: refetch,
},
context: apolloResults ? apolloResults : record,
}); });
}} }}
> >
@@ -177,6 +210,28 @@ export function PaymentsListPaginated({
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
useEffect(() => {
if (search.search && search.search.trim() !== "") {
searchPayments();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function searchPayments(value) {
try {
setSearchLoading(true);
const searchData = await axios.post("/search", {
search: value || search.search,
index: "payments",
});
setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
} catch (error) {
console.log("Error while fetching search results", error);
} finally {
setSearchLoading(false);
}
}
return ( return (
<Card <Card
extra={ extra={
@@ -189,6 +244,7 @@ export function PaymentsListPaginated({
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
delete search.page;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
@@ -212,24 +268,33 @@ export function PaymentsListPaginated({
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history.push({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
searchPayments(value);
}} }}
loading={loading || searchLoading}
enterButton enterButton
/> />
</Space> </Space>
} }
> >
<Table <Table
loading={loading} loading={loading || searchLoading}
scroll={{ x: true }} scroll={{ x: true }}
pagination={{ pagination={
position: "top", search?.search
pageSize: 25, ? {
current: parseInt(page || 1), pageSize: 25,
total: total, showSizeChanger: false,
}} }
: {
pageSize: 25,
current: parseInt(page || 1),
total: total,
showSizeChanger: false,
}
}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
dataSource={payments} dataSource={search?.search ? openSearchResults : payments}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</Card> </Card>

View File

@@ -23,14 +23,34 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
const { id: jobId, job } = printCenterModal.context; const { id: jobId, job } = printCenterModal.context;
const tempList = TemplateList("job", {}); const tempList = TemplateList("job", {});
const { t } = useTranslation(); const { t } = useTranslation();
const JobsReportsList = Object.keys(tempList)
.map((key) => { const JobsReportsList =
return tempList[key]; bodyshop.cdk_dealerid === null && bodyshop.pbs_serialnumber === null
}) ? Object.keys(tempList)
.filter( .map((key) => {
(temp) => return tempList[key];
!temp.regions || (temp.regions && temp.regions[bodyshop.region_config]) })
); .filter(
(temp) =>
(!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions &&
bodyshop.region_config.includes(Object.keys(temp.regions)) ===
true)) &&
(!temp.dms || temp.dms === false)
)
: Object.keys(tempList)
.map((key) => {
return tempList[key];
})
.filter(
(temp) =>
!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions &&
bodyshop.region_config.includes(Object.keys(temp.regions)) ===
true)
);
const filteredJobsReportsList = const filteredJobsReportsList =
search !== "" search !== ""

View File

@@ -0,0 +1,51 @@
import { Col, List, Space, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
const CardColorLegend = ({ bodyshop }) => {
const { t } = useTranslation();
const data = bodyshop.ssbuckets.map((bucket) => {
let color = { r: 255, g: 255, b: 255 };
if (bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return {
label: bucket.label,
color,
};
});
return (
<Col>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{
gutter: 16,
}}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Space>
<div
style={{
width: "1.5rem",
aspectRatio: "1/1",
backgroundColor: `rgba(${item.color.r},${item.color.g},${item.color.b},${item.color.a})`,
}}
></div>
<div>{item.label}</div>
</Space>
</List.Item>
)}
/>
</Col>
);
};
export default CardColorLegend;

View File

@@ -18,6 +18,31 @@ import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.filter(
(bucket) =>
bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true)
)[0];
let color = { r: 255, g: 255, b: 255 };
if (bucket && bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
}
return color;
};
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
return yiq >= 128 ? "black" : "white";
}
export default function ProductionBoardCard( export default function ProductionBoardCard(
technician, technician,
card, card,
@@ -54,10 +79,22 @@ export default function ProductionBoardCard(
.isSame(moment(card.scheduled_completion), "day") && .isSame(moment(card.scheduled_completion), "day") &&
"production-completion-soon")); "production-completion-soon"));
const totalHrs =
card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
return ( return (
<Card <Card
className="react-kanban-card imex-kanban-card" className="react-kanban-card imex-kanban-card"
size="small" size="small"
style={{
backgroundColor:
cardSettings &&
cardSettings.cardcolor &&
`rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color:
cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor),
}}
title={ title={
<Space> <Space>
<ProductionAlert record={card} key="alert" /> <ProductionAlert record={card} key="alert" />

View File

@@ -104,6 +104,13 @@ export default function ProductionBoardKanbanCardSettings({
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.cardcolor")}
name="cardcolor"
>
<Switch />
</Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item <Form.Item
@@ -166,7 +173,7 @@ export default function ProductionBoardKanbanCardSettings({
</div> </div>
); );
return ( return (
<Popover content={overlay} visible={visible}> <Popover content={overlay} visible={visible} placement="topRight">
<Button loading={loading} onClick={() => setVisible(true)}> <Button loading={loading} onClick={() => setVisible(true)}>
{t("production.labels.cardsettings")} {t("production.labels.cardsettings")}
</Button> </Button>

View File

@@ -22,6 +22,7 @@ import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-se
//import "@asseinfo/react-kanban/dist/styles.css"; //import "@asseinfo/react-kanban/dist/styles.css";
import "./production-board-kanban.styles.scss"; import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js"; import { createBoardData } from "./production-board-kanban.utils.js";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician, technician: selectTechnician,
@@ -221,6 +222,7 @@ export function ProductionBoardKanbanComponent({
employeeassignments: true, employeeassignments: true,
scheduled_completion: true, scheduled_completion: true,
stickyheader: false, stickyheader: false,
cardcolor: false,
}; };
return ( return (
@@ -256,6 +258,11 @@ export function ProductionBoardKanbanComponent({
</Space> </Space>
} }
/> />
{cardSettings.cardcolor && (
<CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />
)}
<ProductionListDetailComponent jobs={data} /> <ProductionListDetailComponent jobs={data} />
<StickyContainer> <StickyContainer>
<Board <Board

View File

@@ -91,11 +91,13 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
b.v_make_desc + b.v_model_desc b.v_make_desc + b.v_model_desc
), ),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ <Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
record.v_model_desc || "" record.v_model_yr || ""
} ${record.v_color || ""} ${record.plate_no || ""}`}</span> } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
record.v_color || ""
} ${record.plate_no || ""}`}</Link>
), ),
}, },
{ {

View File

@@ -25,6 +25,7 @@ export default function ProductionListDate({
// } // }
//e.stopPropagation(); //e.stopPropagation();
updateAlert({ updateAlert({
variables: { variables: {
jobId: record.id, jobId: record.id,
@@ -32,6 +33,11 @@ export default function ProductionListDate({
[field]: date, [field]: date,
}, },
}, },
optimisticResponse: {
update_jobs: {
[field]: date,
},
},
}).then(() => { }).then(() => {
if (record.refetch) record.refetch(); if (record.refetch) record.refetch();
if (!time) { if (!time) {
@@ -49,9 +55,11 @@ export default function ProductionListDate({
(moment().add(1, "day").isSame(moment(record[field]), "day") && (moment().add(1, "day").isSame(moment(record[field]), "day") &&
"production-completion-soon")); "production-completion-soon"));
} }
return ( return (
<Dropdown <Dropdown
//trigger={["click"]} trigger={["click"]}
onVisibleChange={(v) => setVisible(v)}
visible={visible} visible={visible}
style={{ style={{
height: "19px", height: "19px",

View File

@@ -1,12 +1,23 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Input, Popover } from "antd"; import { Button, Input, Popover, Space } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa"; import { FaRegStickyNote } from "react-icons/fa";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function ProductionListColumnProductionNote({ record }) { import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setNoteUpsertContext: (context) =>
dispatch(setModalContext({ context: context, modal: "noteUpsert" })),
});
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [note, setNote] = useState( const [note, setNote] = useState(
@@ -60,12 +71,26 @@ export default function ProductionListColumnProductionNote({ record }) {
// onPressEnter={handleSaveNote} // onPressEnter={handleSaveNote}
autoFocus autoFocus
allowClear allowClear
style={{ marginBottom: "1em" }}
/> />
<div> <Space>
<Button onClick={handleSaveNote}> <Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
</div> <Button
onClick={() => {
setVisible(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note,
},
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div> </div>
} }
trigger={["click"]} trigger={["click"]}
@@ -85,3 +110,8 @@ export default function ProductionListColumnProductionNote({ record }) {
</Popover> </Popover>
); );
} }
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListColumnProductionNote);

View File

@@ -55,6 +55,7 @@ export function ProductionListTable({
const assoc = bodyshop.associations.find( const assoc = bodyshop.associations.find(
(a) => a.useremail === currentUser.email (a) => a.useremail === currentUser.email
); );
if (assoc) { if (assoc) {
await updateDefaultProdView({ await updateDefaultProdView({
variables: { assocId: assoc.id, view: value }, variables: { assocId: assoc.id, view: value },

View File

@@ -81,7 +81,7 @@ export function ProductionListTable({
state, state,
activeStatuses: bodyshop.md_ro_statuses.active_statuses, activeStatuses: bodyshop.md_ro_statuses.active_statuses,
}).find((e) => e.key === k.key), }).find((e) => e.key === k.key),
width: k.width, width: k.width ?? 100,
}; };
})) || })) ||
[] []
@@ -246,11 +246,21 @@ export function ProductionListTable({
(x) => x.status === record.status (x) => x.status === record.status
); );
if (!color) return null; if (!color) {
if (index % 2 === 0)
return {
style: {
backgroundColor: `rgb(236, 236, 236)`,
},
};
return null;
}
return { return {
className: "rowWithColor",
style: { style: {
backgroundColor: `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`, "--bgColor": `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`,
}, },
}; };
}, },
@@ -267,6 +277,8 @@ export function ProductionListTable({
sortOrder: sortOrder:
state.sortedInfo.columnKey === c.key && state.sortedInfo.order, state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
title: headerItem(c), title: headerItem(c),
ellipsis: true,
width: c.width ?? 100,
onHeaderCell: (column) => ({ onHeaderCell: (column) => ({
width: column.width, width: column.width,
onResize: handleResize(index), onResize: handleResize(index),
@@ -276,11 +288,12 @@ export function ProductionListTable({
rowKey="id" rowKey="id"
loading={loading} loading={loading}
dataSource={dataSource} dataSource={dataSource}
// scroll={{ x: true }} scroll={{ x: 1000 }}
onChange={handleTableChange} onChange={handleTableChange}
/> />
</ReactDragListView.DragColumn> </ReactDragListView.DragColumn>
</div> </div>
); );
} }
export default connect(mapStateToProps, null)(ProductionListTable); export default connect(mapStateToProps, null)(ProductionListTable);

View File

@@ -3,8 +3,26 @@ import { Resizable } from "react-resizable";
export default function ResizableComponent(props) { export default function ResizableComponent(props) {
const { onResize, width, ...restProps } = props; const { onResize, width, ...restProps } = props;
if (!width) {
return <th {...restProps} />;
}
return ( return (
<Resizable width={width || 200} height={0} onResize={onResize}> <Resizable
width={width || 200}
height={0}
onResize={onResize}
draggableOpts={{ enableUserSelectHack: false }}
handle={
<span
className="react-resizable-handle"
onClick={(e) => {
e.stopPropagation();
}}
/>
}
>
<th {...restProps} /> <th {...restProps} />
</Resizable> </Resizable>
); );

View File

@@ -39,7 +39,7 @@ export default function ProfileShopsComponent({
), ),
}, },
]; ];
console.log("🚀 ~ file: profile-shops.component.jsx:45 ~ data", data);
const filteredData = const filteredData =
search === "" search === ""
? data ? data

View File

@@ -3,7 +3,7 @@ import React from "react";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
QUERY_ALL_ASSOCIATIONS, QUERY_ALL_ASSOCIATIONS,
UPDATE_ASSOCIATION, UPDATE_ACTIVE_ASSOCIATION,
} from "../../graphql/associations.queries"; } from "../../graphql/associations.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ProfileShopsComponent from "./profile-shops.component"; import ProfileShopsComponent from "./profile-shops.component";
@@ -13,9 +13,13 @@ import { getToken } from "firebase/messaging";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -25,14 +29,18 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(ProfileShopsContainer); )(ProfileShopsContainer);
export function ProfileShopsContainer({ bodyshop }) { export function ProfileShopsContainer({ bodyshop, currentUser }) {
const { loading, error, data } = useQuery(QUERY_ALL_ASSOCIATIONS, { const { loading, error, data } = useQuery(QUERY_ALL_ASSOCIATIONS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: {
email: currentUser.email,
},
skip: !currentUser,
}); });
const [updateAssocation] = useMutation(UPDATE_ASSOCIATION); const [updateActiveAssociation] = useMutation(UPDATE_ACTIVE_ASSOCIATION);
const updateActiveShop = async (activeShopId) => { const updateActiveShop = async (newActiveAssocId) => {
logImEXEvent("profile_change_active_shop"); logImEXEvent("profile_change_active_shop");
try { try {
@@ -46,16 +54,12 @@ export function ProfileShopsContainer({ bodyshop }) {
} catch (error) { } catch (error) {
console.log("No FCM token. Skipping unsubscribe."); console.log("No FCM token. Skipping unsubscribe.");
} }
await Promise.all(
data.associations.map(async (record) => { await updateActiveAssociation({
await updateAssocation({ variables: {
variables: { newActiveAssocId: newActiveAssocId,
assocId: record.id, },
assocActive: record.id === activeShopId ? true : false, });
},
});
})
);
//Force window refresh. //Force window refresh.

View File

@@ -26,6 +26,8 @@ const ret = {
"jobs:partsqueue": 4, "jobs:partsqueue": 4,
"jobs:checklist-view": 2, "jobs:checklist-view": 2,
"jobs:list-ready": 1, "jobs:list-ready": 1,
"jobs:void": 5,
"bills:enter": 2, "bills:enter": 2,
"bills:view": 2, "bills:view": 2,
"bills:list": 2, "bills:list": 2,

View File

@@ -92,7 +92,11 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
to: values.to, to: values.to,
subject: Templates[values.key]?.subject, subject: Templates[values.key]?.subject,
}, },
values.sendby === "email" ? "e" : "p", values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id id
); );
setLoading(false); setLoading(false);
@@ -250,15 +254,38 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
ranges={DatePIckerRanges} ranges={DatePIckerRanges}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
label={t("general.labels.sendby")} {() => {
name="sendby" const key = form.getFieldValue("key");
initialValue="print" //Kind of Id
> const reporttype = Templates[key] && Templates[key].reporttype;
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio> if (reporttype === "excel")
<Radio value="print">{t("general.labels.print")}</Radio> return (
</Radio.Group> <Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item> </Form.Item>
<div <div

View File

@@ -12,7 +12,8 @@ import "./schedule-calendar.styles.scss";
import JobDetailCards from "../job-detail-cards/job-detail-cards.component"; import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
import { selectProblemJobs } from "../../redux/application/application.selectors"; import { selectProblemJobs } from "../../redux/application/application.selectors";
import { Alert, Collapse } from "antd"; import { Alert, Collapse } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -66,10 +67,21 @@ export function ScheduleCalendarWrapperComponent({
<Alert <Alert
key={problem.id} key={problem.id}
type="error" type="error"
message={t("appointments.labels.dataconsistency", { message={
ro_number: problem.ro_number, <Trans
code: problem.code, i18nKey="appointments.labels.dataconsistency"
})} components={[
<Link
to={`/manage/jobs/${problem.id}`}
target="_blank"
/>,
]}
values={{
ro_number: problem.ro_number,
code: problem.code,
}}
/>
}
/> />
))} ))}
</Collapse.Panel> </Collapse.Panel>
@@ -79,10 +91,18 @@ export function ScheduleCalendarWrapperComponent({
<Alert <Alert
key={problem.id} key={problem.id}
type="error" type="error"
message={t("appointments.labels.dataconsistency", { message={
ro_number: problem.ro_number, <Trans
code: problem.code, i18nKey="appointments.labels.dataconsistency"
})} components={[
<Link to={`/manage/jobs/${problem.id}`} target="_blank" />,
]}
values={{
ro_number: problem.ro_number,
code: problem.code,
}}
/>
}
/> />
)) ))
)} )}

View File

@@ -1,5 +1,14 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Col, PageHeader, Row, Space } from "antd"; import {
Button,
Card,
Checkbox,
Col,
PageHeader,
Row,
Select,
Space,
} from "antd";
import { t } from "i18next"; import { t } from "i18next";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
@@ -9,24 +18,74 @@ import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component"; import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component"; import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component";
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component"; import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleCalendarComponent);
export default function ScheduleCalendarComponent({ data, refetch }) { export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
const [filter, setFilter] = useLocalStorage("filter_events", { const [filter, setFilter] = useLocalStorage("filter_events", {
intake: true, intake: true,
manual: true, manual: true,
employeevacation: true, employeevacation: true,
ins_co_nm: null,
}); });
const [estimatorsFilter, setEstimatiorsFilter] = useLocalStorage(
"estimators",
[]
);
const estimators = useMemo(() => {
return _.uniq([
...data
.filter((d) => d.__typename === "appointments")
.map((app) =>
`${app.job?.est_ct_fn || ""} ${app.job?.est_ct_ln || ""}`.trim()
)
.filter((e) => e.length > 0),
...bodyshop.md_estimators.map((e) =>
`${e.est_ct_fn || ""} ${e.est_ct_ln || ""}`.trim()
),
]);
}, [data, bodyshop.md_estimators]);
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
return data.filter( return data.filter((d) => {
(d) => const estFilter =
d.block || d.__typename === "appointments"
(filter.intake && d.isintake) || ? estimatorsFilter.length === 0
(filter.manual && !d.isintake && d.block === false) || ? true
(d.__typename === "employee_vacation" && : !!estimatorsFilter.find(
filter.employeevacation && (e) =>
!!d.employee) e ===
); `${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim()
}, [data, filter]); )
: true;
return (
(d.block ||
(filter.intake && d.isintake) ||
(filter.manual && !d.isintake && d.block === false) ||
(d.__typename === "employee_vacation" &&
filter.employeevacation &&
!!d.employee)) &&
(filter.ins_co_nm && filter.ins_co_nm.length > 0
? filter.ins_co_nm.includes(d.job?.ins_co_nm)
: true) &&
estFilter
);
});
}, [data, filter, estimatorsFilter]);
return ( return (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
@@ -37,6 +96,36 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
extra={ extra={
<Space wrap> <Space wrap>
<ScheduleAtsSummary appointments={filteredData} /> <ScheduleAtsSummary appointments={filteredData} />
<Select
style={{ minWidth: "15rem" }}
mode="multiple"
placeholder={t("schedule.labels.estimators")}
allowClear
onClear={() => setEstimatiorsFilter([])}
value={[...estimatorsFilter]}
onChange={(e) => {
setEstimatiorsFilter(e);
}}
options={estimators.map((e) => ({
label: e,
value: e,
}))}
/>
<Select
style={{ minWidth: "15rem" }}
mode="multiple"
placeholder={t("schedule.labels.ins_co_nm_filter")}
allowClear
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
onChange={(e) => {
setFilter({ ...filter, ins_co_nm: e });
}}
options={bodyshop.md_ins_cos.map((i) => ({
label: i.name,
value: i.name,
}))}
/>
<Checkbox <Checkbox
checked={filter?.intake} checked={filter?.intake}
onChange={(e) => { onChange={(e) => {

View File

@@ -148,6 +148,7 @@ export function ScheduleJobModalContainer({
date_scheduled: new Date(), date_scheduled: new Date(),
scheduled_in: values.start, scheduled_in: values.start,
scheduled_completion: values.scheduled_completion, scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
}, },
}, },
}); });

View File

@@ -0,0 +1,39 @@
import Dinero from "dinero.js";
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div
style={{
backgroundColor: "white",
border: "1px solid gray",
padding: "0.5rem",
}}
>
<p style={{ margin: "0" }}>{label}</p>
{payload.map((data, index) => {
if (data.dataKey === "sales" || data.dataKey === "accSales")
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${Dinero({
amount: Math.round(data.value * 100),
}).toFormat()}`}</p>
);
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${data.value}`}</p>
);
})}
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@@ -1,4 +1,6 @@
import { Card } from "antd"; import { Card } from "antd";
import Dinero from "dinero.js";
import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -17,7 +19,7 @@ import {
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util"; import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import _ from "lodash"; import CustomTooltip from "./chart-custom-tooltip";
const graphProps = { const graphProps = {
strokeWidth: 3, strokeWidth: 3,
@@ -44,14 +46,17 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
return { return {
bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs, bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs,
painthrs: dayAcc.painthrs + dayVal.painthrs, painthrs: dayAcc.painthrs + dayVal.painthrs,
sales:
dayAcc.sales + dayVal.job.job_totals.totals.subtotal.amount / 100,
}; };
}, },
{ bodyhrs: 0, painthrs: 0 } { bodyhrs: 0, painthrs: 0, sales: 0 }
); );
} else { } else {
dayhrs = { dayhrs = {
bodyhrs: 0, bodyhrs: 0,
painthrs: 0, painthrs: 0,
sales: 0,
}; };
} }
@@ -64,7 +69,9 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
bodyshop.scoreboard_target.dailyBodyTarget + bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyPaintTarget,
val val
), ) +
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
1 1
), ),
accHrs: _.round( accHrs: _.round(
@@ -73,6 +80,13 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
: dayhrs.painthrs + dayhrs.bodyhrs, : dayhrs.painthrs + dayhrs.bodyhrs,
1 1
), ),
sales: _.round(dayhrs.sales, 2),
accSales: _.round(
acc.length > 0
? acc[acc.length - 1].accSales + dayhrs.sales
: dayhrs.sales,
2
),
}; };
return [...acc, theValue]; return [...acc, theValue];
@@ -87,22 +101,27 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
> >
<CartesianGrid stroke="#f5f5f5" /> <CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} /> <XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis strokeWidth={graphProps.strokeWidth} /> <YAxis
<Tooltip /> strokeWidth={graphProps.strokeWidth}
<Legend /> // allowDataOverflow
<Area dataKey="sales"
type="monotone" yAxisId="right"
name="Accumulated Hours" tickFormatter={(value) =>
dataKey="accHrs" Dinero({ amount: Math.round(value * 100) }).toFormat()
fill="lightgreen" }
stroke="green" orientation="right"
/> />
<YAxis yAxisId="left" strokeWidth={graphProps.strokeWidth} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar <Bar
name="Body Hours" name="Body Hours"
dataKey="bodyHrs" dataKey="bodyHrs"
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="darkblue" fill="darkblue"
yAxisId="left"
/> />
<Bar <Bar
name="Paint Hours" name="Paint Hours"
@@ -110,12 +129,42 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="darkred" fill="darkred"
yAxisId="left"
/> />
<Line <Line
name="Target Hours" name="Target Hours"
type="monotone" type="monotone"
dataKey="accTargetHrs" dataKey="accTargetHrs"
stroke="#ff7300" stroke="#ff7300"
yAxisId="left"
strokeWidth={graphProps.strokeWidth}
/>
<Area
type="monotone"
name="MTD Hours"
dataKey="accHrs"
fill="lightblue"
stroke="blue"
yAxisId="left"
/>
{
// <Area
// type="monotone"
// name="MTD Sales"
// dataKey="accSales"
// fill="lightgreen"
// stroke="green"
// yAxisId="right"
// />
}
<Bar
name="Sales"
dataKey="sales"
stackId="day"
barSize={20}
fill="darkgreen"
yAxisId="right"
strokeWidth={graphProps.strokeWidth} strokeWidth={graphProps.strokeWidth}
/> />
</ComposedChart> </ComposedChart>

View File

@@ -1,4 +1,4 @@
import { Card, Statistic } from "antd"; import { Card, Divider, Statistic } from "antd";
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -41,6 +41,9 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
label="P" label="P"
value={paintHrs.toFixed(1)} value={paintHrs.toFixed(1)}
/> />
<Divider style={{ margin: 0 }} />
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
</Card> </Card>
); );
} }

View File

@@ -1,5 +1,5 @@
import { CalendarOutlined } from "@ant-design/icons"; import { CalendarOutlined } from "@ant-design/icons";
import { Card, Col, Row, Statistic } from "antd"; import { Card, Col, Divider, Row, Statistic } from "antd";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
@@ -177,6 +177,9 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Statistic value={values.toDatePaint.toFixed(1)} /> <Statistic value={values.toDatePaint.toFixed(1)} />
</Col> </Col>
</Row> </Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row> <Row>
<Col {...statSpans}></Col> <Col {...statSpans}></Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -184,14 +187,53 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
value={(values.todayPaint + values.todayBody).toFixed(1)} value={(values.todayPaint + values.todayBody).toFixed(1)}
/> />
</Col> </Col>
<Col {...statSpans}></Col> <Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)} value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/> />
</Col> </Col>
<Col {...statSpans}></Col> <Col {...statSpans}>
<Col {...statSpans}></Col> <Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)} value={(values.toDatePaint + values.toDateBody).toFixed(1)}

View File

@@ -81,6 +81,7 @@ export default function ScoreboardTimeTickets() {
totalLastMonth: 0, totalLastMonth: 0,
totalOverPeriod: 0, totalOverPeriod: 0,
actualTotalOverPeriod: 0, actualTotalOverPeriod: 0,
totalEffieciencyOverPeriod: 0,
employees: {}, employees: {},
}; };
data.fixedperiod.forEach((ticket) => { data.fixedperiod.forEach((ticket) => {
@@ -94,6 +95,7 @@ export default function ScoreboardTimeTickets() {
totalLastMonth: 0, totalLastMonth: 0,
totalOverPeriod: 0, totalOverPeriod: 0,
actualTotalOverPeriod: 0, actualTotalOverPeriod: 0,
totalEffieciencyOverPeriod: 0,
}; };
} }
@@ -221,6 +223,30 @@ export default function ScoreboardTimeTickets() {
ret2.push(r); ret2.push(r);
}); });
// Add total efficiency of employees
const totalActualAndProductive = Object.keys(ret.employees)
.map((key) => {
return { employee_number: key, ...ret.employees[key] };
})
.reduce(
(acc, e) => {
return {
totalOverPeriod: acc.totalOverPeriod + e.totalOverPeriod,
actualTotalOverPeriod:
acc.actualTotalOverPeriod + e.actualTotalOverPeriod,
};
},
{ totalOverPeriod: 0, actualTotalOverPeriod: 0 }
);
ret.totalEffieciencyOverPeriod =
totalActualAndProductive.actualTotalOverPeriod
? (totalActualAndProductive.totalOverPeriod /
totalActualAndProductive.actualTotalOverPeriod) *
100
: 0;
roundObject(ret); roundObject(ret);
roundObject(totals); roundObject(totals);
roundObject(ret2); roundObject(ret2);

View File

@@ -62,7 +62,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
key: "efficiencyoverperiod", key: "efficiencyoverperiod",
render: (text, record) => render: (text, record) =>
`${( `${(
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) * (record.totalOverPeriod / (record.actualTotalOverPeriod || 0.1)) *
100 100
).toFixed(1)} %`, ).toFixed(1)} %`,
}, },
@@ -113,6 +113,12 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
value={data.totalOverPeriod} value={data.totalOverPeriod}
/> />
</Col> </Col>
<Col span={12}>
<Statistic
title={t("scoreboard.labels.efficiencyoverperiod")}
value={`${data.totalEffieciencyOverPeriod || 0}%`}
/>
</Col>
</Row> </Row>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
{t("scoreboard.labels.calendarperiod")} {t("scoreboard.labels.calendarperiod")}
@@ -121,7 +127,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
<Col md={24} lg={20}> <Col md={24} lg={20}>
<Table <Table
columns={columns} columns={columns}
rowKey='employee_number' rowKey="employee_number"
dataSource={tableData} dataSource={tableData}
id="employee_number" id="employee_number"
scroll={{ y: "300px" }} scroll={{ y: "300px" }}

View File

@@ -1,3 +1,4 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Tabs } from "antd"; import { Button, Card, Tabs } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -8,11 +9,14 @@ import ShopInfoGeneral from "./shop-info.general.component";
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component"; import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
import ShopInfoLaborRates from "./shop-info.laborrates.component"; import ShopInfoLaborRates from "./shop-info.laborrates.component";
import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component"; import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component";
import ShopInfoPartsScan from "./shop-info.parts-scan";
import ShopInfoRbacComponent from "./shop-info.rbac.component"; import ShopInfoRbacComponent from "./shop-info.rbac.component";
import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component"; import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component";
import ShopInfoROStatusComponent from "./shop-info.rostatus.component"; import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component"; import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
import ShopInfoSpeedPrint from "./shop-info.speedprint.component"; import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
import { useHistory, useLocation } from "react-router-dom";
import queryString from "query-string";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -23,7 +27,16 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
export function ShopInfoComponent({ bodyshop, form, saveLoading }) { export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
const { CriticalPartsScanning } = useTreatments(
["CriticalPartsScanning"],
{},
bodyshop.imexshopid
);
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const search = queryString.parse(location.search);
return ( return (
<Card <Card
extra={ extra={
@@ -36,7 +49,14 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
</Button> </Button>
} }
> >
<Tabs> <Tabs
defaultActiveKey={search.subtab}
onChange={(key) =>
history.push({
search: `?tab=${search.tab}&subtab=${key}`,
})
}
>
<Tabs.TabPane key="general" tab={t("bodyshop.labels.shopinfo")}> <Tabs.TabPane key="general" tab={t("bodyshop.labels.shopinfo")}>
<ShopInfoGeneral form={form} /> <ShopInfoGeneral form={form} />
</Tabs.TabPane> </Tabs.TabPane>
@@ -71,6 +91,11 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
<Tabs.TabPane key="laborrates" tab={t("bodyshop.labels.laborrates")}> <Tabs.TabPane key="laborrates" tab={t("bodyshop.labels.laborrates")}>
<ShopInfoLaborRates form={form} /> <ShopInfoLaborRates form={form} />
</Tabs.TabPane> </Tabs.TabPane>
{CriticalPartsScanning.treatment === "on" && (
<Tabs.TabPane key="partsscan" tab={t("bodyshop.labels.partsscan")}>
<ShopInfoPartsScan form={form} />
</Tabs.TabPane>
)}
</Tabs> </Tabs>
</Card> </Card>
); );

View File

@@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import { useMutation, useQuery } from "@apollo/client";
import ShopInfoComponent from "./shop-info.component";
import { Form, notification } from "antd"; import { Form, notification } from "antd";
import { useQuery, useMutation } from "@apollo/client"; import moment from "moment";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries"; import React, { useEffect, useState } from "react";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import AlertComponent from "../alert/alert.component";
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import moment from "moment"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ShopInfoComponent from "./shop-info.component";
export default function ShopInfoContainer() { export default function ShopInfoContainer() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,13 +52,28 @@ export default function ShopInfoContainer() {
onFinish={handleFinish} onFinish={handleFinish}
initialValues={ initialValues={
data data
? { ? data.bodyshops[0].accountingconfig.ClosingPeriod
...data.bodyshops[0], ? {
schedule_start_time: moment( ...data.bodyshops[0],
data.bodyshops[0].schedule_start_time accountingconfig: {
), ...data.bodyshops[0].accountingconfig,
schedule_end_time: moment(data.bodyshops[0].schedule_end_time), ClosingPeriod: [
} moment(data.bodyshops[0].accountingconfig.ClosingPeriod[0]),
moment(data.bodyshops[0].accountingconfig.ClosingPeriod[1]),
],
},
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: {
...data.bodyshops[0],
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: null : null
} }
> >

View File

@@ -1,6 +1,8 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { import {
Button, Button,
DatePicker,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@@ -9,24 +11,43 @@ import {
Space, Space,
Switch, Switch,
} from "antd"; } from "antd";
import momentTZ from "moment-timezone";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { import PhoneFormItem, {
PhoneItemFormatterValidation, PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import momentTZ from "moment-timezone";
const timeZonesList = momentTZ.tz.names(); const timeZonesList = momentTZ.tz.names();
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export default function ShopInfoGeneral({ form }) { export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
return ( return (
<div> <div>
<LayoutFormRow header={t("bodyshop.labels.businessinformation")}> <LayoutFormRow
header={t("bodyshop.labels.businessinformation")}
id="businessinformation"
>
<Form.Item <Form.Item
label={t("bodyshop.fields.shopname")} label={t("bodyshop.fields.shopname")}
name="shopname" name="shopname"
@@ -155,7 +176,10 @@ export default function ShopInfoGeneral({ form }) {
<InputNumber min={0} /> <InputNumber min={0} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")}> <LayoutFormRow
header={t("bodyshop.labels.accountingsetup")}
id="accountingsetup"
>
<Form.Item <Form.Item
label={t("bodyshop.labels.qbo")} label={t("bodyshop.labels.qbo")}
valuePropName="checked" valuePropName="checked"
@@ -385,8 +409,25 @@ export default function ShopInfoGeneral({ form }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item> </Form.Item>
{ClosingPeriod.treatment === "on" && (
<>
<Form.Item
allowClear
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePickerRanges}
/>
</Form.Item>
</>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")}> <LayoutFormRow
header={t("bodyshop.labels.scoreboardsetup")}
id="scoreboardsetup"
>
<Form.Item <Form.Item
label={t("bodyshop.fields.dailypainttarget")} label={t("bodyshop.fields.dailypainttarget")}
name={["scoreboard_target", "dailyPaintTarget"]} name={["scoreboard_target", "dailyPaintTarget"]}
@@ -445,7 +486,10 @@ export default function ShopInfoGeneral({ form }) {
<InputNumber min={1} precision={1} /> <InputNumber min={1} precision={1} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.systemsettings")}> <LayoutFormRow
header={t("bodyshop.labels.systemsettings")}
id="systemsettings"
>
<Form.Item <Form.Item
name={["md_referral_sources"]} name={["md_referral_sources"]}
label={t("bodyshop.fields.md_referral_sources")} label={t("bodyshop.fields.md_referral_sources")}
@@ -473,6 +517,13 @@ export default function ShopInfoGeneral({ form }) {
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
name={["enforce_conversion_category"]}
label={t("bodyshop.fields.enforce_conversion_category")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item <Form.Item
name={["target_touchtime"]} name={["target_touchtime"]}
label={t("bodyshop.fields.target_touchtime")} label={t("bodyshop.fields.target_touchtime")}
@@ -544,6 +595,13 @@ export default function ShopInfoGeneral({ form }) {
> >
<CurrencyInput /> <CurrencyInput />
</Form.Item> </Form.Item>
<Form.Item
name={["use_paint_scale_data"]}
label={t("bodyshop.fields.use_paint_scale_data")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item <Form.Item
name={["attach_pdf_to_email"]} name={["attach_pdf_to_email"]}
label={t("bodyshop.fields.attach_pdf_to_email")} label={t("bodyshop.fields.attach_pdf_to_email")}
@@ -575,6 +633,20 @@ export default function ShopInfoGeneral({ form }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item> </Form.Item>
<Form.Item
name={["md_email_cc", "parts_return_slip"]}
label={t("bodyshop.fields.md_email_cc", {
template: "parts_return_slip",
})}
rules={[
{
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="tags" />
</Form.Item>
<Form.Item <Form.Item
name={["tt_allow_post_to_invoiced"]} name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")} label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
@@ -582,6 +654,13 @@ export default function ShopInfoGeneral({ form }) {
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
name={["tt_enforce_hours_for_tech_console"]}
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item <Form.Item
name={["bill_allow_post_to_closed"]} name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")} label={t("bodyshop.fields.bill_allow_post_to_closed")}
@@ -634,7 +713,11 @@ export default function ShopInfoGeneral({ form }) {
<Input /> <Input />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}> <LayoutFormRow
grow
header={t("bodyshop.labels.messagingpresets")}
id="messagingpresets"
>
<Form.List name={["md_messaging_presets"]}> <Form.List name={["md_messaging_presets"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -699,7 +782,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.notespresets")}> <LayoutFormRow
grow
header={t("bodyshop.labels.notespresets")}
id="notespresets"
>
<Form.List name={["md_notes_presets"]}> <Form.List name={["md_notes_presets"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -764,7 +851,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.partslocations")}> <LayoutFormRow
grow
header={t("bodyshop.labels.partslocations")}
id="partslocations"
>
<Form.List name={["md_parts_locations"]}> <Form.List name={["md_parts_locations"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -818,7 +909,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.insurancecos")}> <LayoutFormRow
grow
header={t("bodyshop.labels.insurancecos")}
id="insurancecos"
>
<Form.List name={["md_ins_cos"]}> <Form.List name={["md_ins_cos"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -914,7 +1009,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.estimators")}> <LayoutFormRow
grow
header={t("bodyshop.labels.estimators")}
id="estimators"
>
<Form.List name={["md_estimators"]}> <Form.List name={["md_estimators"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -1003,7 +1102,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.filehandlers")}> <LayoutFormRow
grow
header={t("bodyshop.labels.filehandlers")}
id="filehandlers"
>
<Form.List name={["md_filehandlers"]}> <Form.List name={["md_filehandlers"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -1085,7 +1188,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.fields.md_ccc_rates")}> <LayoutFormRow
grow
header={t("bodyshop.fields.md_ccc_rates")}
id="md_ccc_rates"
>
<Form.List name={["md_ccc_rates"]}> <Form.List name={["md_ccc_rates"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -1202,7 +1309,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.fields.md_jobline_presets")}> <LayoutFormRow
grow
header={t("bodyshop.fields.md_jobline_presets")}
id="md_jobline_presets"
>
<Form.List name={["md_jobline_presets"]}> <Form.List name={["md_jobline_presets"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -1383,7 +1494,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.fields.md_parts_order_comment")}> <LayoutFormRow
grow
header={t("bodyshop.fields.md_parts_order_comment")}
id="md_parts_order_comment"
>
<Form.List name={["md_parts_order_comment"]}> <Form.List name={["md_parts_order_comment"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -1449,7 +1564,11 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.md_to_emails")}> <LayoutFormRow
grow
header={t("bodyshop.labels.md_to_emails")}
id="md_to_emails"
>
<Form.List name={["md_to_emails"]}> <Form.List name={["md_to_emails"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (

View File

@@ -20,7 +20,10 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
const TemplateListGenerated = TemplateList(); const TemplateListGenerated = TemplateList();
return ( return (
<div> <div>
<LayoutFormRow header={t("bodyshop.labels.intakechecklist")}> <LayoutFormRow
header={t("bodyshop.labels.intakechecklist")}
id="intakechecklist"
>
<Form.List name={["intakechecklist", "form"]}> <Form.List name={["intakechecklist", "form"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -188,7 +191,10 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
</Form.Item> </Form.Item>
</SelectorDiv> </SelectorDiv>
<LayoutFormRow header={t("bodyshop.labels.deliverchecklist")}> <LayoutFormRow
header={t("bodyshop.labels.deliverchecklist")}
id="deliverchecklist"
>
<Form.List name={["deliverchecklist", "form"]}> <Form.List name={["deliverchecklist", "form"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (

View File

@@ -0,0 +1,81 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
export default function ShopInfoPartsScan({ form }) {
const { t } = useTranslation();
return (
<div>
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
<Form.List name={["md_parts_scan"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.expression")}
key={`${index}expression`}
name={[field.name, "expression"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.flags")}
key={`${index}flags`}
name={[field.name, "flags"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.addpartsrule")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div>
);
}

View File

@@ -1,12 +1,12 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { Form, InputNumber } from "antd"; import { Form, InputNumber } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import { useTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -316,6 +316,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.void")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "jobs:void"]}
>
<InputNumber />
</Form.Item>
<Form.Item <Form.Item
label={t("bodyshop.fields.rbac.bills.enter")} label={t("bodyshop.fields.rbac.bills.enter")}
rules={[ rules={[

View File

@@ -95,7 +95,6 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
{form.getFieldValue("pbs_serialnumber")} {form.getFieldValue("pbs_serialnumber")}
</DataLabel> </DataLabel>
)} )}
<LayoutFormRow> <LayoutFormRow>
<Form.Item <Form.Item
label={t("bodyshop.fields.dms.default_journal")} label={t("bodyshop.fields.dms.default_journal")}
@@ -163,6 +162,32 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Switch /> <Switch />
</Form.Item> </Form.Item>
)} )}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.appostingaccount")}
name={["pbs_configuration", "appostingaccount"]}
>
<Select
options={[
{ value: "wip", label: "WIP" },
{ value: "cogs", label: "COGS" },
]}
/>
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.apcontrol")}
name={["pbs_configuration", "apcontrol"]}
>
<Select
options={[
{ value: "ro", label: "RO Number" },
{ value: "vendordmsid", label: "Vendor DMS ID" },
]}
/>
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}> <LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}> <Form.List name={["cdk_configuration", "payers"]}>
@@ -217,7 +242,9 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
{t("jobs.fields.ponumber")} {t("jobs.fields.ponumber")}
</Select.Option> </Select.Option>
<Select.Option value="account_number"> <Select.Option value="account_number">
{t("jobs.fields.dms.control_type.account_number")} {t(
"jobs.fields.dms.control_type.account_number"
)}
</Select.Option> </Select.Option>
</Select> </Select>
</Form.Item> </Form.Item>
@@ -313,7 +340,10 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</DataLabel> </DataLabel>
</> </>
)} )}
<LayoutFormRow header={t("bodyshop.labels.responsibilitycenters.costs")}> <LayoutFormRow
header={t("bodyshop.labels.responsibilitycenters.costs")}
id="costs"
>
<Form.List name={["md_responsibility_centers", "costs"]}> <Form.List name={["md_responsibility_centers", "costs"]}>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
@@ -423,6 +453,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("bodyshop.fields.dms.dms_control_override")}
key={`${index}dms_control_override`}
name={[field.name, "dms_control_override"]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled <DeleteFilled
onClick={() => { onClick={() => {
@@ -451,6 +490,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<LayoutFormRow <LayoutFormRow
header={t("bodyshop.labels.responsibilitycenters.profits")} header={t("bodyshop.labels.responsibilitycenters.profits")}
id="profits"
> >
<Form.List name={["md_responsibility_centers", "profits"]}> <Form.List name={["md_responsibility_centers", "profits"]}>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
@@ -546,6 +586,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("bodyshop.fields.dms.dms_control_override")}
key={`${index}dms_control_override`}
name={[field.name, "dms_control_override"]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
<DeleteFilled <DeleteFilled
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
@@ -581,7 +630,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
{fields.map((field, index) => ( {fields.map((field, index) => (
<Form.Item key={field.key}> <Form.Item key={field.key}>
<div> <div>
<LayoutFormRow> <LayoutFormRow id="mappingname">
<Form.Item <Form.Item
label={t("bodyshop.fields.dms.mappingname")} label={t("bodyshop.fields.dms.mappingname")}
key={`${index}name`} key={`${index}name`}
@@ -611,6 +660,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow <LayoutFormRow
header={t("bodyshop.labels.defaultcostsmapping")} header={t("bodyshop.labels.defaultcostsmapping")}
id="defaultcostsmapping"
> >
<Form.Item <Form.Item
label={t( label={t(
@@ -4068,6 +4118,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<LayoutFormRow <LayoutFormRow
header={t("bodyshop.labels.responsibilitycenters.tax_accounts")} header={t("bodyshop.labels.responsibilitycenters.tax_accounts")}
id="tax_accounts"
> >
<Form.Item <Form.Item
label={t("bodyshop.fields.responsibilitycenters.federal_tax")} label={t("bodyshop.fields.responsibilitycenters.federal_tax")}
@@ -4182,7 +4233,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
{DmsAp.treatment === "on" && ( {DmsAp.treatment === "on" && (
<LayoutFormRow> <LayoutFormRow id="federal_tax_itc">
<Form.Item <Form.Item
label={t("bodyshop.fields.responsibilitycenters.federal_tax_itc")} label={t("bodyshop.fields.responsibilitycenters.federal_tax_itc")}
rules={[ rules={[
@@ -4296,7 +4347,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
)} )}
<LayoutFormRow> <LayoutFormRow id="state_tax">
<Form.Item <Form.Item
label={t("bodyshop.fields.responsibilitycenters.state_tax")} label={t("bodyshop.fields.responsibilitycenters.state_tax")}
rules={[ rules={[
@@ -4394,7 +4445,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<InputNumber precision={2} /> <InputNumber precision={2} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow> <LayoutFormRow id="local_tax">
<Form.Item <Form.Item
label={t("bodyshop.fields.responsibilitycenters.local_tax")} label={t("bodyshop.fields.responsibilitycenters.local_tax")}
rules={[ rules={[
@@ -4492,7 +4543,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<InputNumber precision={2} /> <InputNumber precision={2} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={<div>AR</div>}> <LayoutFormRow header={<div>AR</div>} id="AR">
{/* <Form.Item {/* <Form.Item
label={t("bodyshop.fields.responsibilitycenters.ar")} label={t("bodyshop.fields.responsibilitycenters.ar")}
rules={[ rules={[
@@ -4556,7 +4607,10 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</LayoutFormRow> </LayoutFormRow>
{DmsAp.treatment === "on" && ( {DmsAp.treatment === "on" && (
<LayoutFormRow header={t("bodyshop.fields.responsibilitycenters.ap")}> <LayoutFormRow
header={t("bodyshop.fields.responsibilitycenters.ap")}
id="ap"
>
{/* <Form.Item {/* <Form.Item
label={t("bodyshop.fields.responsibilitycenters.ap")} label={t("bodyshop.fields.responsibilitycenters.ap")}
rules={[ rules={[
@@ -4619,7 +4673,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
)} )}
<LayoutFormRow header={<div>Refund</div>}> <LayoutFormRow header={<div>Refund</div>} id="refund">
{/* <Form.Item {/* <Form.Item
label={t("bodyshop.fields.responsibilitycenters.refund")} label={t("bodyshop.fields.responsibilitycenters.refund")}
rules={[ rules={[
@@ -4682,7 +4736,10 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
{Qb_Multi_Ar.treatment === "on" && ( {Qb_Multi_Ar.treatment === "on" && (
<LayoutFormRow header={<div>Multiple Payers Item</div>}> <LayoutFormRow
header={<div>Multiple Payers Item</div>}
id="accountitem"
>
<Form.Item <Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")} label={t("bodyshop.fields.responsibilitycenter_accountitem")}
rules={[ rules={[
@@ -4710,7 +4767,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Form.Item key={field.key}> <Form.Item key={field.key}>
<LayoutFormRow> <LayoutFormRow id="sales_tax_codes">
<Form.Item <Form.Item
label={t( label={t(
"bodyshop.fields.responsibilitycenters.sales_tax_codes.description" "bodyshop.fields.responsibilitycenters.sales_tax_codes.description"

View File

@@ -44,7 +44,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
}; };
return ( return (
<SelectorDiv> <SelectorDiv id="jobstatus">
<Form.Item <Form.Item
name={["md_ro_statuses", "statuses"]} name={["md_ro_statuses", "statuses"]}
label={t("bodyshop.labels.alljobstatuses")} label={t("bodyshop.labels.alljobstatuses")}
@@ -322,6 +322,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
<LayoutFormRow <LayoutFormRow
grow grow
header={t("bodyshop.fields.statuses.production_colors")} header={t("bodyshop.fields.statuses.production_colors")}
id="production_colors"
> >
<Form.List name={["md_ro_statuses", "production_colors"]}> <Form.List name={["md_ro_statuses", "production_colors"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
@@ -396,7 +397,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
); );
} }
const ColorPicker = ({ value, onChange, style, ...restProps }) => { export const ColorPicker = ({ value, onChange, style, ...restProps }) => {
const handleChange = (color) => { const handleChange = (color) => {
if (onChange) onChange(color.rgb); if (onChange) onChange(color.rgb);
}; };

View File

@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component"; import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { ColorPicker } from "./shop-info.rostatus.component";
export default function ShopInfoSchedulingComponent({ form }) { export default function ShopInfoSchedulingComponent({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -76,9 +77,22 @@ export default function ShopInfoSchedulingComponent({ form }) {
> >
<InputNumber min={0} /> <InputNumber min={0} />
</Form.Item> </Form.Item>
<Form.Item
name={["md_lost_sale_reasons"]}
label={t("bodyshop.fields.md_lost_sale_reasons")}
rules={[
{
// required: true,
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="tags" />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Divider orientation="left">{t("bodyshop.labels.workingdays")}</Divider> <Divider orientation="left">{t("bodyshop.labels.workingdays")}</Divider>
<Space wrap size="large"> <Space wrap size="large" id="workingdays">
<Form.Item <Form.Item
label={t("general.labels.sunday")} label={t("general.labels.sunday")}
name={["workingdays", "sunday"]} name={["workingdays", "sunday"]}
@@ -129,7 +143,7 @@ export default function ShopInfoSchedulingComponent({ form }) {
<Switch /> <Switch />
</Form.Item> </Form.Item>
</Space> </Space>
<LayoutFormRow header={t("bodyshop.labels.apptcolors")}> <LayoutFormRow header={t("bodyshop.labels.apptcolors")} id="apptcolors">
<Form.List name={["appt_colors"]}> <Form.List name={["appt_colors"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -194,7 +208,7 @@ export default function ShopInfoSchedulingComponent({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.ssbuckets")}> <LayoutFormRow header={t("bodyshop.labels.ssbuckets")} id="ssbuckets">
<Form.List name={["ssbuckets"]}> <Form.List name={["ssbuckets"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
return ( return (
@@ -264,17 +278,50 @@ export default function ShopInfoSchedulingComponent({ form }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Space wrap>
<DeleteFilled <Space direction="horizontal">
onClick={() => { <Form.Item
remove(field.name); label={
}} <Space>
/> {t("bodyshop.fields.ssbuckets.color")}
<FormListMoveArrows <Button
move={move} size="small"
index={index} onClick={() => {
total={fields.length} form.setFieldValue([
/> "ssbuckets",
field.name,
"color",
]);
form.setFields([
{
name: ["ssbuckets", field.name, "color"],
touched: true,
},
]);
}}
>
Reset
</Button>
</Space>
}
key={`${index}color`}
name={[field.name, "color"]}
>
<ColorPicker />
</Form.Item>
<Space wrap>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</Space> </Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>

View File

@@ -42,7 +42,9 @@ export default function ShopUsersAuthEdit({ association }) {
</div> </div>
)} )}
{!visible && ( {!visible && (
<div style={{ cursor: "pointer" }} onClick={() => setVisible(true)}> <div
style={{ cursor: "pointer" }} //onClick={() => setVisible(true)}
>
{association.authlevel || t("general.labels.na")} {association.authlevel || t("general.labels.na")}
</div> </div>
)} )}

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