Compare commits

..

2 Commits

Author SHA1 Message Date
Patrick Fic
3f070af7ab Merge branch 'release/2022-07-15' of bitbucket.org:snaptsoft/bodyshop into release/2022-07-15 2022-07-20 14:16:43 -07:00
Patrick Fic
bc4b6fa007 IO-1988 Download LMS as zip. 2022-07-20 14:16:41 -07:00
96 changed files with 17476 additions and 24526 deletions

View File

@@ -19,6 +19,3 @@ npx deadfile ./src/index.js --exclude build templates
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
hasura migrate apply --version "1620771761757" --skip-execution --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-license-file --input package.json --output third-party-licenses.txt --overwrite

View File

@@ -1149,48 +1149,6 @@
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>cc</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>contents</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>created</name>
<definition_loaded>false</definition_loaded>
@@ -1233,48 +1191,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>subject</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>to</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>useremail</name>
<definition_loaded>false</definition_loaded>
@@ -11233,27 +11149,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dateinpast</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>dlexpirebeforereturn</name>
<definition_loaded>false</definition_loaded>
@@ -12475,27 +12370,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>leasereturn</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>out</name>
<definition_loaded>false</definition_loaded>
@@ -14861,27 +14735,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>vacationadded</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -27448,27 +27301,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>emailaudit</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>employeeassignments</name>
<definition_loaded>false</definition_loaded>
@@ -27511,27 +27343,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimator</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>existing_jobs</name>
<definition_loaded>false</definition_loaded>
@@ -32732,48 +32543,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>systemnotes</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>usernotes</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -32930,27 +32699,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>saving</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>selectexistingornew</name>
<definition_loaded>false</definition_loaded>
@@ -33355,27 +33103,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tax_number</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -40660,27 +40387,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_not_recieved_vendor</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>payments_by_date</name>
<definition_loaded>false</definition_loaded>
@@ -40933,27 +40639,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>psr_by_make</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>purchases_by_cost_center_detail</name>
<definition_loaded>false</definition_loaded>
@@ -41384,69 +41069,6 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>employeevacation</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>intake</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>manual</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>manualevent</name>
<definition_loaded>false</definition_loaded>
@@ -41724,48 +41346,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>efficiencyoverperiod</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>entries</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</name>
<definition_loaded>false</definition_loaded>

View File

@@ -4,18 +4,18 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@apollo/client": "^3.6.9",
"@apollo/client": "^3.6.6",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.5",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.7.0",
"@sentry/tracing": "^7.7.0",
"@splitsoftware/splitio-react": "^1.6.0",
"@stripe/react-stripe-js": "^1.9.0",
"@stripe/stripe-js": "^1.32.0",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.22.3",
"@sentry/react": "^7.1.1",
"@sentry/tracing": "^7.1.1",
"@splitsoftware/splitio-react": "^1.4.1",
"@stripe/react-stripe-js": "^1.8.1",
"@stripe/stripe-js": "^1.31.0",
"@tanem/react-nprogress": "^5.0.1",
"antd": "^4.21.0",
"apollo-link-logger": "^2.0.0",
"axios": "^0.27.2",
"craco-less": "^1.20.0",
@@ -24,33 +24,33 @@
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.9.1",
"firebase": "^9.8.2",
"graphql": "^16.5.0",
"i18next": "^21.8.14",
"i18next": "^21.8.9",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.9.0",
"jsoneditor": "^9.8.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.9",
"logrocket": "^3.0.1",
"markerjs2": "^2.22.0",
"libphonenumber-js": "^1.10.6",
"logrocket": "^3.0.0",
"markerjs2": "^2.21.4",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"normalize-url": "^7.0.3",
"phone": "^3.1.23",
"phone": "^3.1.20",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",
"query-string": "^7.1.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.2",
"react-big-calendar": "^1.5.0",
"react-big-calendar": "^0.40.1",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.2.1",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.18.1",
"react-i18next": "^11.17.0",
"react-icons": "^4.4.0",
"react-number-format": "^4.9.3",
"react-redux": "^7.2.8",
@@ -60,13 +60,13 @@
"react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.12",
"recharts": "^2.1.10",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.4",
"redux-state-sync": "^3.1.2",
"reselect": "^4.1.6",
"sass": "^1.54.0",
"sass": "^1.51.0",
"socket.io-client": "^4.5.1",
"styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0",
@@ -119,9 +119,9 @@
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.19.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.1",
"@sentry/webpack-plugin": "^1.18.9",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.6.1",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -142,9 +142,3 @@
}
}
}
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important;
}

View File

@@ -4,8 +4,6 @@ import { useQuery } from "@apollo/client";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
import { Card, Row } from "antd";
export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
@@ -20,20 +18,10 @@ export default function AuditTrailListContainer({ recordId }) {
{error ? (
<AlertComponent type="error" message={error.message} />
) : (
<Row gutter={[16, 16]}>
<Card>
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
<Card>
<EmailAuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
</Row>
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : null}
/>
)}
</div>
);

View File

@@ -1,64 +0,0 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",
key: " created",
width: "10%",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created,
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
},
];
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
}

View File

@@ -1,251 +0,0 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
});
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<>
{loading && <LoadingSkeleton />}
{data && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<BillDetailEditReturn data={data} />
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -1,185 +0,0 @@
import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditReturn);
export function BillDetailEditReturn({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
data,
disabled,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines
.filter((l) => selectedLines.includes(l.id))
.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
delete search.billid;
history.push({ search: queryString.stringify(search) });
setVisible(false);
};
useEffect(() => {
if (visible === false) form.resetFields();
}, [visible, form]);
return (
<>
<Modal
visible={visible}
onCancel={() => setVisible(false)}
destroyOnClose
title={t("bills.actions.return")}
onOk={() => form.submit()}
>
<Form
initialValues={data && data.bills_by_pk}
onFinish={handleFinish}
form={form}
>
<Form.List name={["billlines"]}>
{(fields, { add, remove, move }) => {
return (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<td>
<Checkbox
onChange={(e) => {
form.setFieldsValue({
billlines: form
.getFieldsValue()
.billlines.map((b) => ({
...b,
selected: e.target.checked,
})),
});
}}
/>
</td>
<td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_price")}</td>
<td>{t("billlines.fields.actual_cost")}</td>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.selected")}
key={`${index}selected`}
name={[field.name, "selected"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
);
}}
</Form.List>
</Form>
</Modal>
<Button
disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => {
setVisible(true);
}}
>
{t("bills.actions.return")}
</Button>
</>
);
}

View File

@@ -1,12 +1,68 @@
import { Drawer, Grid } from "antd";
import { useMutation, useQuery } from "@apollo/client";
import {
Button,
Drawer,
Form,
Grid,
PageHeader,
Popconfirm,
Space,
} from "antd";
import moment from "moment";
import queryString from "query-string";
import React from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
import { createStructuredSelector } from "reselect";
import {
DELETE_BILL_LINE,
INSERT_NEW_BILL_LINES,
UPDATE_BILL_LINE,
} from "../../graphql/bill-lines.queries";
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
export default function BillDetailEditcontainer() {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
@@ -24,6 +80,114 @@ export default function BillDetailEditcontainer() {
? bpoints[selectedBreakpoint[0]]
: "100%";
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
//Find bill lines that were deleted.
const deletedJobLines = [];
data.bills_by_pk.billlines.forEach((a) => {
const matchingRecord = billlines.find((b) => b.id === a.id);
if (!matchingRecord) {
deletedJobLines.push(a);
}
});
deletedJobLines.forEach((d) => {
updates.push(deleteBillLine({ variables: { id: d.id } }));
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setVisible(false);
setUpdateLoading(false);
};
useEffect(() => {
if (search.billid && data) {
form.resetFields();
}
}, [form, search.billid, data]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return (
<Drawer
width={drawerPercentage}
@@ -31,10 +195,119 @@ export default function BillDetailEditcontainer() {
delete search.billid;
history.push({ search: queryString.stringify(search) });
}}
destroyOnClose
visible={search.billid}
>
<BillDetailEditComponent />
{loading && <LoadingSkeleton />}
{!loading && (
<>
<PageHeader
title={
data &&
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
}
extra={
<Space>
<Button
disabled={data.bills_by_pk.is_credit_memo}
onClick={() => {
delete search.billid;
history.push({ search: queryString.stringify(search) });
setPartsOrderContext({
actions: {},
context: {
jobId: data.bills_by_pk.jobid,
vendorId: data.bills_by_pk.vendorid,
returnFromBill: data.bills_by_pk.id,
invoiceNumber: data.bills_by_pk.invoice_number,
linesToOrder: data.bills_by_pk.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
<Popconfirm
visible={visible}
onConfirm={() => form.submit()}
onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")}
>
<Button
htmlType="submit"
disabled={exported}
onClick={handleSave}
loading={updateLoading}
type="primary"
>
{t("general.actions.save")}
</Button>
</Popconfirm>
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
<BillMarkExportedButton bill={data && data.bills_by_pk} />
</Space>
}
/>
<Form
form={form}
onFinish={handleFinish}
initialValues={transformData(data)}
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null}
/>
) : (
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
)}
</Form>
</>
)}
</Drawer>
);
}
const transformData = (data) => {
return data
? {
...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => {
return {
...i,
joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
},
};
}),
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
}
: {};
};

View File

@@ -1,14 +1,14 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button, Form,
Button,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip
Tooltip,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -17,8 +17,9 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser

View File

@@ -27,10 +27,6 @@ const BillLineSearchSelect = (
option.oem_partno
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.alt_partno &&
option.alt_partno
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.act_price &&
option.act_price.toString().startsWith(inputValue.toString()))
);
@@ -52,17 +48,14 @@ const BillLineSearchSelect = (
line_desc={item.line_desc}
part_qty={item.part_qty}
oem_partno={item.oem_partno}
alt_partno={item.alt_partno}
act_price={item.act_price}
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span>
<span>{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}`}</span>
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`

View File

@@ -12,7 +12,6 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
@@ -59,15 +58,39 @@ export function BillsListTableComponent({
</Button>
)}
<BillDeleteButton bill={record} />
<BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }}
<Button
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
/>
onClick={() => {
setPartsOrderContext({
actions: {},
context: {
jobId: job.id,
vendorId: record.vendorid,
returnFromBill: record.id,
invoiceNumber: record.invoice_number,
linesToOrder: record.billlines.map((i) => {
return {
line_desc: i.line_desc,
// db_price: i.actual_price,
act_price: i.actual_price,
cost: i.actual_cost,
quantity: i.quantity,
joblineid: i.joblineid,
oem_partno: i.jobline && i.jobline.oem_partno,
part_type: i.jobline && i.jobline.part_type,
};
}),
isReturn: true,
},
});
}}
>
{t("bills.actions.return")}
</Button>
{record.isinhouse && (
<PrintWrapperComponent
templateObject={{

View File

@@ -9,7 +9,6 @@ import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
@@ -59,24 +58,17 @@ export function ChatMediaSelector({
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{!bodyshop.uselocalmediaserver && data && (
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && visible && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={
conversation.job_conversations[0] &&
conversation.job_conversations[0].jobid
}
/>
)}
</div>
);
if (bodyshop.uselocalmediaserver) return null;
return (
<Popover
content={

View File

@@ -279,80 +279,31 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
<Input.TextArea />
</Form.Item>
<div>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.registrationexpires !== c.registrationexpires
}
>
{() => {
const expires = form.getFieldValue("registrationexpires");
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<div>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
shouldUpdate={(p, c) =>
p.insuranceexpires !== c.insuranceexpires
}
>
{() => {
const expires = form.getFieldValue("insuranceexpires");
const dateover = expires && moment(expires).isBefore(moment());
if (dateover)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.dateinpast")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
</div>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>

View File

@@ -34,9 +34,6 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
<Option value="courtesycars.status.sold">
{t("courtesycars.status.sold")}
</Option>
<Option value="courtesycars.status.leasereturn">
{t("courtesycars.status.leasereturn")}
</Option>
</Select>
);
};

View File

@@ -52,10 +52,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
text: t("courtesycars.status.sold"),
value: "courtesycars.status.sold",
},
{
text: t("courtesycars.status.leasereturn"),
value: "courtesycars.status.leasereturn",
},
],
onFilter: (value, record) => value.includes(record.status),
sortOrder:

View File

@@ -5,15 +5,12 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
@@ -28,7 +25,6 @@ export function EmailDocumentsComponent({
emailConfig,
form,
selectedMediaState,
bodyshop,
}) {
const { t } = useTranslation();
@@ -56,18 +52,12 @@ export function EmailDocumentsComponent({
10485760 - new Blob([form.getFieldValue("html")]).size ? (
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
) : null}
{!bodyshop.uselocalmediaserver && data && (
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && (
<JobsDocumentsLocalGalleryExternalComponent
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={emailConfig.jobid}
/>
)}
</div>
);
}

View File

@@ -160,13 +160,14 @@ export function EmailOverlayComponent({
</Form.Item>
<Tabs>
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
{!bodyshop.uselocalmediaserver && (
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
)}
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>

View File

@@ -77,9 +77,6 @@ export function EmailOverlayContainer({
setSending(true);
try {
await axios.post("/sendemail", {
bodyshopid: bodyshop.id,
jobid: emailConfig.jobid,
...defaultEmailFrom,
ReplyTo: {
Email: from,
@@ -184,11 +181,10 @@ export function EmailOverlayContainer({
loading: sending,
disabled:
selectedMedia &&
(selectedMedia
( (selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ||
selectedMedia.filter((s) => s.isSelected).length > 10),
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
}}
>
<Form layout="vertical" form={form} onFinish={handleFinish}>

View File

@@ -5,7 +5,7 @@ import AlertComponent from "../alert/alert.component";
import { Prompt, useLocation } from "react-router-dom";
import "./form-fields-changed.styles.scss";
export default function FormsFieldChanged({ form, skipPrompt }) {
export default function FormsFieldChanged({ form }) {
const { t } = useTranslation();
const handleReset = () => {
@@ -25,7 +25,7 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Prompt
when={skipPrompt ? false : true}
when={true}
message={(location) => {
if (loc.pathname === location.pathname) return false;
return t("general.messages.unsavedchangespopup");

View File

@@ -1,24 +1,13 @@
import { useQuery } from "@apollo/client";
import { Button, Card, Col, Row, Table, Tag } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { Card, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail);
export function JobAuditTrail({ currentUser, jobId }) {
export default function JobAuditTrail({ jobId }) {
const { t } = useTranslation();
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { jobid: jobId },
skip: !jobId,
fetchPolicy: "network-only",
@@ -45,104 +34,15 @@ export function JobAuditTrail({ currentUser, jobId }) {
key: "operation",
},
];
const emailColumns = [
{
title: t("audit.fields.created"),
dataIndex: " created_at",
key: " created_at",
render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
},
{
title: t("audit.fields.to"),
dataIndex: "to",
key: "to",
render: (text, record) =>
record.to &&
record.to.map((email, idx) => <Tag key={idx}>{email}</Tag>),
},
{
title: t("audit.fields.cc"),
dataIndex: "cc",
key: "cc",
render: (text, record) =>
record.cc &&
record.cc.map((email, idx) => <Tag key={idx}>{email}</Tag>),
},
{
title: t("audit.fields.subject"),
dataIndex: "subject",
key: "subject",
},
...(currentUser?.email.includes("@imex.")
? [
{
title: t("audit.fields.contents"),
dataIndex: "contents",
key: "contents",
width: "10%",
render: (text, record) => (
<Button
onClick={() => {
var win = window.open(
"",
"Title",
"toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=400,"
);
win.document.body.innerHTML = record.contents;
}}
>
Preview
</Button>
),
},
]
: []),
];
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<Card
title={t("jobs.labels.audit")}
extra={
<Button
onClick={() => {
refetch();
}}
>
<SyncOutlined />
</Button>
}
>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.emailaudit")}>
<Table
loading={loading}
columns={emailColumns}
rowKey="id"
dataSource={data ? data.email_audit_trail : []}
/>
</Card>
</Col>
</Row>
<Card title={t("jobs.labels.audit")}>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
);
}

View File

@@ -115,10 +115,7 @@ export function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
render: (text, record) => record.oem_partno,
},
{
title: t("joblines.fields.op_code_desc"),
@@ -144,26 +141,11 @@ export function JobLinesComponent({
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
value: ["PAN", "PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
@@ -173,29 +155,9 @@ export function JobLinesComponent({
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
value: ["PAS", "PASL"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
@@ -381,11 +343,7 @@ export function JobLinesComponent({
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleMark = (e) => {
@@ -503,12 +461,7 @@ export function JobLinesComponent({
context: {
jobId: job.id,
job: job,
linesToOrder: selectedLines.map((l) => ({
...l,
oem_partno: `${l.oem_partno || ""} ${
l.alt_partno ? `(${l.alt_partno})` : ""
}`.trim(),
})),
linesToOrder: selectedLines,
},
});
@@ -521,24 +474,12 @@ export function JobLinesComponent({
</Button>
<Button
onClick={() => {
setState((state) => ({
setState({
...state,
filteredInfo: {
...state.filteredInfo,
part_type: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
},
}));
});
}}
>
<FilterFilled /> {t("jobs.actions.filterpartsonly")}

View File

@@ -66,8 +66,10 @@ export function JobLineConvertToLabor({
const newAdjustments = _.cloneDeep(
existingAdjustments.data.jobs_by_pk.lbr_adjustments
);
const adjustment = calculateAdjustment({ mod_lbr_ty, job, jobline });
newAdjustments[mod_lbr_ty] = (newAdjustments[mod_lbr_ty] || 0) + adjustment;
newAdjustments[mod_lbr_ty] =
(newAdjustments[mod_lbr_ty] || 0) +
calculateAdjustment({ mod_lbr_ty, job, jobline });
const jobUpdate = client.mutate({
mutation: UPDATE_JOB,
@@ -81,13 +83,7 @@ export function JobLineConvertToLabor({
mutation: UPDATE_JOB_LINE,
variables: {
lineId: jobline.id,
line: {
convertedtolbr: true,
convertedtolbr_data: {
mod_lbr_ty: mod_lbr_ty,
mod_lb_hrs: adjustment,
},
},
line: { convertedtolbr: true },
},
});

View File

@@ -1,4 +1,3 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import React, { useEffect, useState } from "react";
@@ -38,8 +37,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id, job: values },
refetchQueries: ['GET_JOB_BY_PK'],
awaitRefetchQueries:true
});
const changedAuditFields = form.getFieldsValue(
@@ -68,8 +65,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
}),
});
}
form.resetFields();
form.resetFields();
setLoading(false);
//Get the owner details, populate it all back into the job.
};
@@ -92,7 +87,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
: null,
}}
>
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item
label={t("jobs.fields.date_estimated")}

View File

@@ -95,7 +95,7 @@ mutation UNVOID_JOB($jobId: uuid!) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(),
operation: AuditTrailMapping.admin_unvoicejob(),
});
} else {
notification["error"]({

View File

@@ -45,11 +45,9 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
return acc + generateUpdateQuery(value, idx);
}, "");
const removeQueries = existingLines
.filter((l) => !l.manual_line)
.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
const removeQueries = existingLines.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
console.log(insertQueries, updateQueries, removeQueries);
if ((insertQueries + updateQueries + removeQueries).trim() === "") {

View File

@@ -205,7 +205,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
const HasBeenConvertedTolabor = ({ value }) => {
const { t } = useTranslation();
console.log(value);
if (!value) return null;
return (
<Tooltip title={t("joblines.labels.convertedtolabor")}>

View File

@@ -1,163 +0,0 @@
import { useMutation } from "@apollo/client";
import {
Button,
Card,
Form,
Input,
Menu,
notification,
Popover,
Select,
Space,
} from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDetailHeaderAddEvent);
export function JobsDetailHeaderAddEvent({ bodyshop, jobid, ...props }) {
const { t } = useTranslation();
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const handleFinish = async (values) => {
logImEXEvent("schedule_manual_event");
setLoading(true);
try {
insertAppointment({
variables: {
apt: { ...values, isintake: false, jobid, bodyshopid: bodyshop.id },
},
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
notification.open({
type: "success",
message: t("appointments.successes.created"),
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
setVisibility(false);
}
};
const overlay = (
<Card>
<div>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("appointments.fields.title")}
name="title"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note">
<Input />
</Form.Item>
<Form.Item
label={t("appointments.fields.start")}
name="start"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDateTimePickerComponent
onBlur={() => {
const start = form.getFieldValue("start");
form.setFieldsValue({ end: start.add(30, "minutes") });
}}
/>
</Form.Item>
<Form.Item
label={t("appointments.fields.end")}
name="end"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
if (value) {
const { start } = form.getFieldsValue();
if (moment(start).isAfter(moment(value))) {
return Promise.reject(
t("employees.labels.endmustbeafterstart")
);
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>
<Button type="primary" htmlType="submit" loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
</Card>
);
const handleClick = (e) => {
setVisibility(true);
};
return (
<Popover content={overlay} visible={visibility}>
<Menu.Item {...props} onClick={handleClick}>
{t("appointments.labels.manualevent")}
</Menu.Item>
</Popover>
);
}

View File

@@ -15,7 +15,6 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -422,7 +421,6 @@ export function JobsDetailHeaderActions({
</Popconfirm>
</Menu.Item>
)}
<JobsDetailHeaderActionsAddevent jobid={job.id} />
{!jobRO && job.converted && (
<Menu.Item>
<Popconfirm

View File

@@ -1,10 +1,10 @@
import { Card, Col, Row, Space, Tag } from "antd";
import {
WarningFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
WarningFilled,
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tag } from "antd";
import React from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
@@ -13,17 +13,17 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -56,6 +56,12 @@ const colSpan = {
export function JobsDetailHeader({ job, bodyshop, disabled }) {
const { t } = useTranslation();
const jobInPostProduction = useMemo(() => {
return bodyshop.md_ro_statuses.post_production_statuses.includes(
job.status
);
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
const vehicleTitle = `${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim();
@@ -123,10 +129,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
{(job.inproduction || jobInPostProduction) && (
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
)}
<Space>
{job.special_coverage_policy && (
<Tag color="tomato">
@@ -174,11 +181,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
{job.ownr_ea || ""}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
</div>
</Card>
</Col>

View File

@@ -1,4 +1,4 @@
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
@@ -58,24 +58,6 @@ export function JobsDocumentsLocalGallery({
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
const jobMedia =
allMedia && allMedia[job.id]
? allMedia[job.id].reduce(
(acc, val) => {
if (
val.type &&
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.images.push(val);
} else {
acc.other.push(val);
}
return acc;
},
{ images: [], other: [] }
)
: { images: [], other: [] };
return (
<div>
<Space wrap>
@@ -108,7 +90,7 @@ export function JobsDocumentsLocalGallery({
</Card>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={jobMedia.images}
images={(allMedia && allMedia[job.id]) || []}
backdropClosesModal={true}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
@@ -122,31 +104,6 @@ export function JobsDocumentsLocalGallery({
}}
/>
</Card>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={jobMedia.other}
backdropClosesModal={true}
enableLightbox={false}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled />,
height: "100%",
width: "100%",
cursor: "pointer",
};
}}
onClickThumbnail={(index) => {
window.open(
jobMedia.other[index].src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
/>
</Card>
</div>
);
}

View File

@@ -1,79 +0,0 @@
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
getJobMedia,
toggleMediaSelected,
} from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
toggleMediaSelected: ({ jobid, filename }) =>
dispatch(toggleMediaSelected({ jobid, filename })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobDocumentsLocalGalleryExternal);
function JobDocumentsLocalGalleryExternal({
jobId,
externalMediaState,
getJobMedia,
toggleMediaSelected,
allMedia,
}) {
const [galleryImages, setgalleryImages] = externalMediaState;
const { t } = useTranslation();
useEffect(() => {
if ( jobId) {
getJobMedia(jobId);
}
}, [jobId, getJobMedia]);
useEffect(() => {
let documents =
allMedia && allMedia[jobId]
? allMedia[jobId].reduce((acc, val) => {
if (
val.type &&
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.push(val);
}
return acc;
}, [])
: [];
setgalleryImages(documents);
}, [allMedia, jobId, setgalleryImages, t]);
return (
<div className="clearfix">
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelectImage={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
)
);
}}
/>
</div>
);
}

View File

@@ -77,12 +77,6 @@ export function JobsList({ bodyshop }) {
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
@@ -270,32 +264,6 @@ export function JobsList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",

View File

@@ -14,7 +14,6 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import useLocalStorage from "../../utils/useLocalStorage";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -41,8 +40,6 @@ export function JobNotesComponent({
relatedRos,
}) {
const { t } = useTranslation();
const [filter, setFilter] = useLocalStorage("filter_job_notes_icons", null);
const Templates = TemplateList("job_special", {
ro_number,
});
@@ -53,18 +50,6 @@ export function JobNotesComponent({
dataIndex: "icons",
key: "icons",
width: 80,
filteredValue: filter?.icons || null,
filters: [
{
text: t("notes.labels.usernotes"),
value: false,
},
{
text: t("notes.labels.systemnotes"),
value: true,
},
],
onFilter: (value, record) => record.audit === value,
render: (text, record) => (
<span>
{record.critical ? (
@@ -146,10 +131,6 @@ export function JobNotesComponent({
},
];
const handleTableChange = (pagination, filters, sorter) => {
setFilter(filters);
};
return (
<div>
<LayoutFormRow>
@@ -185,7 +166,6 @@ export function JobNotesComponent({
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
</Card>
</div>

View File

@@ -89,12 +89,6 @@ export function JobsReadyList({ bodyshop }) {
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.est_ct_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
@@ -282,32 +276,6 @@ export function JobsReadyList({ bodyshop }) {
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.labels.estimator"),
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
filterSearch: true,
filters:
(jobs &&
jobs
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
.filter(onlyUnique)
.map((s) => {
return {
text: s || "N/A",
value: [s],
};
})) ||
[],
onFilter: (value, record) =>
value.includes(
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
),
render: (text, record) =>
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",

View File

@@ -1,13 +1,12 @@
import { EditFilled } from "@ant-design/icons";
import { Card, Col, Row, Space, Table, Typography } from "antd";
import { Card, Col, Row, Space, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss";
@@ -45,10 +44,10 @@ export function LaborAllocationsTable({
if (!jobId) setTotals([]);
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
const convertedLines = useMemo(
() => joblines && joblines.filter((j) => j.convertedtolbr),
[joblines]
);
// const convertedLines = useMemo(
// () => joblines && joblines.filter((j) => j.convertedtolbr),
// [joblines]
// );
const columns = [
{
@@ -121,86 +120,57 @@ export function LaborAllocationsTable({
),
},
];
const convertedTableCols = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
ellipsis: true,
},
{
title: t("joblines.fields.op_code_desc"),
dataIndex: "op_code_desc",
key: "op_code_desc",
ellipsis: true,
render: (text, record) =>
`${record.op_code_desc || ""}${
record.alt_partm ? ` ${record.alt_partm}` : ""
}`,
},
// const convertedTableCols = [
// {
// title: t("joblines.fields.line_desc"),
// dataIndex: "line_desc",
// key: "line_desc",
// ellipsis: true,
// },
// {
// title: t("joblines.fields.op_code_desc"),
// dataIndex: "op_code_desc",
// key: "op_code_desc",
// ellipsis: true,
// render: (text, record) =>
// `${record.op_code_desc || ""}${
// record.alt_partm ? ` ${record.alt_partm}` : ""
// }`,
// },
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
ellipsis: true,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
) : (
<></>
)}
</>
),
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.mod_lbr_ty"),
dataIndex: "conv_mod_lbr_ty",
key: "conv_mod_lbr_ty",
render: (text, record) =>
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "conv_mod_lb_hrs",
key: "conv_mod_lb_hrs",
render: (text, record) =>
record.convertedtolbr_data &&
record.convertedtolbr_data.mod_lb_hrs &&
record.convertedtolbr_data.mod_lb_hrs.toFixed(1),
},
];
// {
// title: t("joblines.fields.act_price"),
// dataIndex: "act_price",
// key: "act_price",
// ellipsis: true,
// render: (text, record) => (
// <>
// <CurrencyFormatter>
// {record.db_ref === "900510" || record.db_ref === "900511"
// ? record.prt_dsmk_m
// : record.act_price}
// </CurrencyFormatter>
// {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
// <span
// style={{ marginLeft: ".2rem" }}
// >{`(${record.prt_dsmk_p}%)`}</span>
// ) : (
// <></>
// )}
// </>
// ),
// },
// {
// title: t("joblines.fields.part_qty"),
// dataIndex: "part_qty",
// key: "part_qty",
// },
// ];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const summary =
totals &&
totals.reduce(
(acc, val) => {
acc.hrs_total += val.total;
acc.hrs_claimed += val.claimed;
acc.adjustments += val.adjustments;
acc.difference += val.difference;
return acc;
},
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
);
return (
<Row gutter={[16, 16]}>
<Col span={24}>
@@ -214,45 +184,26 @@ export function LaborAllocationsTable({
scroll={{
x: true,
}}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.hrs_total.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.hrs_claimed.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.adjustments.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.difference.toFixed(1)}
</Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</Card>
</Col>
{convertedLines && convertedLines.length > 0 && (
<Col span={24}>
<Card title={t("jobs.labels.convertedtolabor")}>
<Table
columns={convertedTableCols}
rowKey="id"
pagination={false}
dataSource={convertedLines}
scroll={{
x: true,
}}
/>
</Card>
</Col>
)}
{
// convertedLines && convertedLines.length > 0 && (
// <Col span={24}>
// <Card title={t("jobs.labels.convertedtolabor")}>
// <Table
// columns={convertedTableCols}
// rowKey="id"
// pagination={false}
// dataSource={convertedLines}
// scroll={{
// x: true,
// }}
// />
// </Card>
// </Col>
// )
}
</Row>
);
}

View File

@@ -6,17 +6,12 @@ export const CalculateAllocationsTotals = (
timetickets,
adjustments = []
) => {
console.log(
"🚀 ~ file: labor-allocations-table.utility.js ~ line 9 ~ adjustments",
adjustments
);
const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const ticketCodes = timetickets.map((item) => item.ciecacode);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const adjustmentCodes = Object.keys(adjustments);
const allCodes = [...jobCodes, ...ticketCodes, ...adjustmentCodes].filter(
const allCodes = [...jobCodes, ...ticketCodes].filter(
(value, index, self) => self.indexOf(value) === index && !!value
);
@@ -39,7 +34,7 @@ export const CalculateAllocationsTotals = (
}, 0),
};
r.difference = r.total + r.adjustments - r.claimed;
r.difference = (r.total + r.adjustments - r.claimed).toFixed(2);
acc.push(r);
return acc;
}, []);

View File

@@ -94,12 +94,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("owners.fields.tax_number")}
name="tax_number"
>
<Input />
</Form.Item>
</LayoutFormRow>
<Form.Item label={t("owners.fields.note")} name="note">
<Input.TextArea rows={4} />

View File

@@ -20,10 +20,9 @@ function OwnerDetailFormContainer({ owner, refetch }) {
if (!!result.errors) {
notification["error"]({
message: t("owners.errors.saving", {
error: JSON.stringify(result.errors),
message: JSON.stringify(result.errors),
}),
});
setLoading(false);
return;
}

View File

@@ -12,24 +12,24 @@ export default connect(mapStateToProps, mapDispatchToProps)(OwnerNameDisplay);
export function OwnerNameDisplay({ bodyshop, ownerObject }) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
if (bodyshop.last_name_first)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
const emptyTest =
ownerObject?.ownr_fn + ownerObject?.ownr_ln + ownerObject?.ownr_co_nm;
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
if (!emptyTest || emptyTest === "null" || emptyTest.trim() === "")
return "N/A";
@@ -37,11 +37,11 @@ export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
const rdxStore = store.getState();
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
return `${ownerObject?.ownr_ln || ""}, ${ownerObject?.ownr_fn || ""} ${
ownerObject?.ownr_co_nm || ""
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
return `${ownerObject?.ownr_fn || ""} ${ownerObject?.ownr_ln || ""} ${
ownerObject?.ownr_co_nm || ""
return `${ownerObject.ownr_fn || ""} ${ownerObject.ownr_ln || ""} ${
ownerObject.ownr_co_nm || ""
}`.trim();
}

View File

@@ -238,8 +238,10 @@ export function ProductionListTable({
sticky
pagination={false}
size="small"
{...(Production_List_Status_Colors.treatment === "on" && {
onRow: (record, index) => {
className="production-list-table"
onRow={
Production_List_Status_Colors.treatment === "on" &&
((record, index) => {
if (!bodyshop.md_ro_statuses.production_colors) return null;
const color = bodyshop.md_ro_statuses.production_colors.find(
@@ -253,8 +255,8 @@ export function ProductionListTable({
backgroundColor: `rgb(${color.color.r},${color.color.g},${color.color.b},${color.color.a})`,
},
};
},
})}
})
}
components={{
header: {
cell: ResizeableTitle,

View File

@@ -39,12 +39,7 @@ export function ScheduleCalendarHeaderComponent({
const ATSToday = useMemo(() => {
if (!events) return [];
return _.groupBy(
events.filter(
(e) =>
!e.vacation &&
e.isintake &&
moment(date).isSame(moment(e.start), "day")
),
events.filter((e) => moment(date).isSame(moment(e.start), "day")),
"job.alt_transport"
);
}, [events, date]);

View File

@@ -1,8 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Col, PageHeader, Row, Space } from "antd";
import { t } from "i18next";
import React, { useMemo } from "react";
import useLocalStorage from "../../utils/useLocalStorage";
import { Button, Card, Col, PageHeader, Row, Space } from "antd";
import React from "react";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
@@ -10,23 +8,6 @@ import ScheduleProductionList from "../schedule-production-list/schedule-product
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
export default function ScheduleCalendarComponent({ data, refetch }) {
const [filter, setFilter] = useLocalStorage("filter_events", {
intake: true,
manual: true,
employeevacation: true,
});
const filteredData = useMemo(() => {
return data.filter(
(d) =>
d.block ||
(filter.intake && d.isintake) ||
(filter.manual && !d.isintake && d.block === false) ||
(d.__typename === "employee_vacation" &&
filter.employeevacation &&
!!d.employee)
);
}, [data, filter]);
return (
<Row gutter={[16, 16]}>
<ScheduleModal />
@@ -35,30 +16,6 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<PageHeader
extra={
<Space wrap>
<Checkbox
checked={filter?.intake}
onChange={(e) => {
setFilter({ ...filter, intake: e.target.checked });
}}
>
{t("schedule.labels.intake")}
</Checkbox>
<Checkbox
checked={filter?.manual}
onChange={(e) => {
setFilter({ ...filter, manual: e.target.checked });
}}
>
{t("schedule.labels.manual")}
</Checkbox>
<Checkbox
checked={filter?.employeevacation}
onChange={(e) => {
setFilter({ ...filter, employeevacation: e.target.checked });
}}
>
{t("schedule.labels.employeevacation")}
</Checkbox>
<ScheduleVerifyIntegrity />
<Button
onClick={() => {
@@ -78,7 +35,7 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<Col span={24}>
<Card>
<ScheduleCalendarWrapperComponent
data={filteredData}
data={data}
refetch={refetch}
style={{ height: "100rem" }}
/>

View File

@@ -1,42 +1,42 @@
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
import React, { useState } from "react";
import { Dropdown, Button, Table, Space, Card, Input } from "antd";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
import { SyncOutlined } from "@ant-design/icons";
import { DateFormatter } from "../../utils/DateFormatter";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation();
const [state, setState] = useState({
visible: false,
search: "",
current: 1,
pageSize: 10,
});
const [searchText, setSearchText] = useState("");
const { loading, error, data, refetch } = useQuery(
QUERY_SCOREBOARD_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !state.visible,
variables: {
search: state.search !== "" ? `%${state.search}%` : null,
offset: state.current ? (state.current - 1) * state.pageSize : 0,
limit: state.pageSize,
order: [
{
date: "desc",
},
],
},
}
);
const jobs = scoreBoardlist
? searchText === ""
? scoreBoardlist
: scoreBoardlist.filter(
(sb) =>
(sb.job.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const columns = [
{
@@ -97,69 +97,35 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
},
];
const overlay = (
<Card
style={{ maxWidth: "90vw", padding: "1rem" }}
onClick={(e) => e.stopPropagation()}
extra={
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
onClick={(e) => e.stopPropagation()}
/>
}
>
<Table
pagination={false}
columns={columns}
rowKey="id"
dataSource={jobs}
onClick={(e) => e.stopPropagation()}
/>
</Card>
);
return (
<>
<Modal
visible={state.visible}
destroyOnClose
width="80%"
cancelButtonProps={{ style: { display: "none" } }}
onCancel={() =>
setState((state) => ({
...state,
visible: false,
current: 1,
search: "",
}))
}
>
{error && (
<AlertComponent type="error" message={JSON.stringify(error)} />
)}
<Card
extra={
<Space align="middle" wrap>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: state.search })}
</Typography.Title>
<Input.Search
placeholder={t("jobs.fields.ro_number")}
allowClear
onSearch={(value) => {
setState((state) => ({ ...state, search: value }));
}}
//value={state.search}
enterButton
/>
</Space>
}
>
<Table
columns={columns}
rowKey="id"
dataSource={data ? data.scoreboard : []}
loading={loading}
onClick={(e) => e.stopPropagation()}
onChange={(tableArgs) =>
setState((state) => ({ ...state, ...tableArgs }))
}
pagination={{
position: "top",
pageSize: state.pageSize || 10,
current: state.current || 1,
total: data ? data.scoreboard_aggregate.aggregate.count : 0,
}}
/>
</Card>
</Modal>
<Button
onClick={() => setState((state) => ({ ...state, visible: true }))}
>
{t("scoreboard.labels.entries")}
</Button>
</>
<Dropdown trigger={["click"]} overlay={overlay}>
<Button>Jobs</Button>
</Dropdown>
);
}

View File

@@ -17,8 +17,6 @@ export default function ScoreboardRemoveButton({ scoreboardId }) {
setLoading(true);
const result = await deleteScoreboardEntry({
variables: { sbId: scoreboardId },
awaitRefetchQueries: true,
refetchQueries: ["QUERY_SCOREBOARD_PAGINATED"],
});
if (!!result.errors) {

View File

@@ -80,7 +80,6 @@ export default function ScoreboardTimeTickets() {
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
actualTotalOverPeriod: 0,
employees: {},
};
data.fixedperiod.forEach((ticket) => {
@@ -93,7 +92,6 @@ export default function ScoreboardTimeTickets() {
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
actualTotalOverPeriod: 0,
};
}
@@ -184,9 +182,6 @@ export default function ScoreboardTimeTickets() {
ret.employees[ticket.employee.employee_number].totalOverPeriod =
ret.employees[ticket.employee.employee_number].totalOverPeriod +
ticket.productivehrs;
ret.employees[ticket.employee.employee_number].actualTotalOverPeriod =
ret.employees[ticket.employee.employee_number]
.actualTotalOverPeriod + (ticket.actualhrs || 0);
if (!totals.employees[ticket.employee.employee_number])
totals.employees[ticket.employee.employee_number] = {
@@ -224,7 +219,7 @@ export default function ScoreboardTimeTickets() {
roundObject(ret);
roundObject(totals);
roundObject(ret2);
console.log(ret);
return {
fixed: ret,
timeperiod: {

View File

@@ -19,6 +19,7 @@ export default connect(
export function ScoreboardTicketsStats({ data, bodyshop }) {
const { t } = useTranslation();
console.log(data);
const columns = [
{
title: t("employees.fields.employee_number"),
@@ -56,16 +57,6 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
key: "totalOverPeriod",
sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod,
},
{
title: t("scoreboard.labels.efficiencyoverperiod"),
dataIndex: "efficiencyoverperiod",
key: "efficiencyoverperiod",
render: (text, record) =>
`${(
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) *
100
).toFixed(1)} %`,
},
];
const tableData = data
@@ -121,7 +112,6 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
<Col md={24} lg={20}>
<Table
columns={columns}
rowKey='employee_number'
dataSource={tableData}
id="employee_number"
scroll={{ y: "300px" }}

View File

@@ -43,10 +43,9 @@ export default function ShopEmployeeAddVacation({ employee }) {
});
} else {
notification["success"]({
message: t("employees.successes.vacationadded"),
message: t("employees.successes.added"),
});
}
form.resetFields();
setLoading(false);
setVisibility(false);
};

View File

@@ -1494,7 +1494,7 @@ export default function ShopInfoGeneral({ form }) {
}
const ReceivableCustomFieldSelect = (
<Select allowClear>
<Select>
<Select.Option value="v_vin">VIN</Select.Option>
<Select.Option value="clm_no">Claim No.</Select.Option>
<Select.Option value="ded_amt">Deductible Amount</Select.Option>

View File

@@ -2,7 +2,7 @@ import React from "react";
export default function VehicleVinDisplay({ children }) {
if (!children) return null;
console.log(children);
if (typeof children !== "string" || children.length !== 17) return children;
const vin = children.trim();

View File

@@ -1,77 +1,75 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
) {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } },
{ end: { _lte: $endd } },
{_and:[{start:{_lte: $startd}},{end:{_gte:$endd}}]}] }
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
) {
id
start
end
employee {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } }, { end: { _lte: $endd } }] }
) {
id
last_name
first_name
start
end
employee {
id
last_name
first_name
}
}
}
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
}
`;

View File

@@ -13,20 +13,6 @@ export const QUERY_AUDIT_TRAIL = gql`
created
bodyshopid
}
email_audit_trail(
where: { jobid: { _eq: $jobid } }
order_by: { created_at: desc }
) {
cc
contents
created_at
id
jobid
noteid
subject
to
useremail
}
}
`;

View File

@@ -23,7 +23,6 @@ export const GET_ALL_JOBLINES_BY_PK = gql`
notes
location
tax_part
manual_line
}
}
`;
@@ -50,7 +49,6 @@ export const GET_LINE_TICKET_BY_PK = gql`
lbr_amt
op_code_desc
convertedtolbr
convertedtolbr_data
}
timetickets(where: { jobid: { _eq: $id } }) {
actualhrs
@@ -181,7 +179,6 @@ export const UPDATE_JOB_LINE = gql`
status
removed
convertedtolbr
convertedtolbr_data
}
}
}
@@ -198,7 +195,6 @@ export const GET_JOB_LINES_TO_ENTER_BILL = gql`
line_desc
part_type
oem_partno
alt_partno
db_price
act_price
part_qty
@@ -208,7 +204,6 @@ export const GET_JOB_LINES_TO_ENTER_BILL = gql`
lbr_op
lbr_amt
op_code_desc
alt_partno
}
jobs_by_pk(id: $id) {
id

View File

@@ -40,8 +40,6 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
updated_at
ded_amt
suspended
est_ct_fn
est_ct_ln
}
}
`;
@@ -623,7 +621,6 @@ export const GET_JOB_BY_PK = gql`
ownr_ctry
ownr_ph1
ownr_ph2
tax_number
}
labor_rate_desc
rate_la1
@@ -687,7 +684,6 @@ export const GET_JOB_BY_PK = gql`
line_ref
part_type
oem_partno
alt_partno
db_price
act_price
part_qty
@@ -859,7 +855,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
id
allow_text_message
preferred_contact
tax_number
}
vehicleid
v_model_yr
@@ -1912,7 +1907,6 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
profitcenter_part
prt_dsmk_p
convertedtolbr
convertedtolbr_data
}
}
}

View File

@@ -67,7 +67,6 @@ export const QUERY_OWNER_BY_ID = gql`
ownr_zip
preferred_contact
note
tax_number
jobs {
id
ro_number

View File

@@ -88,41 +88,3 @@ export const GET_BLOCKED_DAYS = gql`
}
}
`;
export const QUERY_SCOREBOARD_PAGINATED = gql`
query QUERY_SCOREBOARD_PAGINATED(
$search: String
$offset: Int
$limit: Int
$order: [scoreboard_order_by!]
) {
scoreboard(
where: { job: { ro_number: { _ilike: $search } } }
offset: $offset
limit: $limit
order_by: $order
) {
id
jobid
job {
id
ro_number
invoice_date
v_make_desc
v_model_desc
v_model_yr
ownr_fn
ownr_ln
ownr_co_nm
}
date
bodyhrs
painthrs
}
scoreboard_aggregate(where: { job: { ro_number: { _ilike: $search } } }) {
aggregate {
count(distinct: true)
}
}
}
`;

View File

@@ -79,7 +79,6 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
date
id
rate
actualhrs
productivehrs
memo
jobid

View File

@@ -1,15 +1,12 @@
import React from "react";
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
export default function TechLookupContainer() {
return (
<div>
<RbacWrapperComponent action="jobs:list-active">
<TechLookupJobsList />
<TechLookupJobsDrawer />
</RbacWrapperComponent>
<TechLookupJobsList />
<TechLookupJobsDrawer />
</div>
);
}

View File

@@ -17,9 +17,7 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({
const sagaMiddleWare = createSagaMiddleware();
const reduxSyncConfig = {
whitelist: [
"ADD_RECENT_ITEM", //"SET_SHOP_DETAILS"
],
whitelist: ["ADD_RECENT_ITEM", "SET_SHOP_DETAILS"],
};
const middlewares = [

View File

@@ -269,7 +269,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
try {
const userEmail = yield select((state) => state.user.currentUser.email);
try {
//console.log("Setting shop timezone.");
console.log("Setting shop timezone.");
// moment.tz.setDefault(payload.timezone);
} catch (error) {
console.log(error);

View File

@@ -82,12 +82,8 @@
},
"audit": {
"fields": {
"cc": "CC",
"contents": "Contents",
"created": "Time",
"operation": "Operation",
"subject": "Subject",
"to": "To",
"useremail": "User",
"values": "Values"
}
@@ -682,7 +678,6 @@
"refuelqty": "Refuel qty.?"
},
"correctdataonform": "Please review the information above. If any of it is not correct, you can fix it later.",
"dateinpast": "Date is in the past.",
"dlexpirebeforereturn": "The driver's license expires before the car is expected to return. ",
"driverinformation": "Driver's Information",
"findcontract": "Find Contract",
@@ -757,7 +752,6 @@
"status": {
"in": "Available",
"inservice": "In Service",
"leasereturn": "Lease Returned",
"out": "Rented",
"sold": "Sold"
},
@@ -926,8 +920,7 @@
},
"successes": {
"delete": "Employee deleted successfully.",
"save": "Employee saved successfully.",
"vacationadded": "Employee vacation added."
"save": "Employee saved successfully."
},
"validation": {
"unique_employee_number": "You must enter a unique employee number."
@@ -1618,10 +1611,8 @@
"documents-images": "Images",
"documents-other": "Other Documents",
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
"emailaudit": "Email Audit Trail",
"employeeassignments": "Employee Assignments",
"estimatelines": "Estimate Lines",
"estimator": "Estimator",
"existing_jobs": "Existing Jobs",
"federal_tax_amt": "Federal Taxes",
"gpdollars": "$ G.P.",
@@ -1925,9 +1916,7 @@
"labels": {
"addtorelatedro": "Add to Related ROs",
"newnoteplaceholder": "Add a note...",
"notetoadd": "Note to Add",
"systemnotes": "System Notes",
"usernotes": "User Notes"
"notetoadd": "Note to Add"
},
"successes": {
"create": "Note created successfully.",
@@ -1946,7 +1935,6 @@
},
"errors": {
"noaccess": "The record does not exist or you do not have access to it. ",
"saving": "Error saving owner. {{error}}.",
"selectexistingornew": "Select an existing owner record or create a new one. "
},
"fields": {
@@ -1967,8 +1955,7 @@
"ownr_st": "Province/State",
"ownr_title": "Title",
"ownr_zip": "Zip/Postal Code",
"preferred_contact": "Preferred Contact Method",
"tax_number": "Tax Number"
"preferred_contact": "Preferred Contact Method"
},
"forms": {
"address": "Address",
@@ -2411,7 +2398,6 @@
"open_orders_status": "Open Orders by Status",
"parts_backorder": "IOU Parts List",
"parts_not_recieved": "Parts Not Received",
"parts_not_recieved_vendor": "Parts Not Received by Vendor",
"payments_by_date": "Payments by Date",
"payments_by_date_type": "Payments by Date and Type",
"production_by_category": "Production by Category",
@@ -2424,7 +2410,6 @@
"production_by_target_date": "Production by Target Date",
"production_by_technician": "Production by Technician",
"production_by_technician_one": "Production filtered by Technician",
"psr_by_make": "Percent of Sales by Vehicle Make",
"purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)",
"purchases_by_cost_center_summary": "Purchases by Cost Center (Summary)",
"purchases_by_date_range_detail": "Purchases by Date - Detail",
@@ -2449,9 +2434,6 @@
},
"schedule": {
"labels": {
"employeevacation": "Employee Vacations",
"intake": "Intake Events",
"manual": "Manual Events",
"manualevent": "Add Manual Event"
}
},
@@ -2474,8 +2456,6 @@
"calendarperiod": "Periods based on calendar weeks/months.",
"dailyactual": "Actual (D)",
"dailytarget": "Daily",
"efficiencyoverperiod": "Efficiency over Selected Dates",
"entries": "Scoreboard Entries",
"jobs": "Jobs",
"lastmonth": "Last Month",
"lastweek": "Last Week",

View File

@@ -82,12 +82,8 @@
},
"audit": {
"fields": {
"cc": "",
"contents": "",
"created": "",
"operation": "",
"subject": "",
"to": "",
"useremail": "",
"values": ""
}
@@ -682,7 +678,6 @@
"refuelqty": ""
},
"correctdataonform": "",
"dateinpast": "",
"dlexpirebeforereturn": "",
"driverinformation": "",
"findcontract": "",
@@ -757,7 +752,6 @@
"status": {
"in": "",
"inservice": "",
"leasereturn": "",
"out": "",
"sold": ""
},
@@ -926,8 +920,7 @@
},
"successes": {
"delete": "Empleado eliminado con éxito.",
"save": "Empleado guardado con éxito.",
"vacationadded": ""
"save": "Empleado guardado con éxito."
},
"validation": {
"unique_employee_number": ""
@@ -1618,10 +1611,8 @@
"documents-images": "",
"documents-other": "",
"duplicateconfirm": "",
"emailaudit": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
"existing_jobs": "Empleos existentes",
"federal_tax_amt": "",
"gpdollars": "",
@@ -1925,9 +1916,7 @@
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Agrega una nota...",
"notetoadd": "",
"systemnotes": "",
"usernotes": ""
"notetoadd": ""
},
"successes": {
"create": "Nota creada con éxito.",
@@ -1946,7 +1935,6 @@
},
"errors": {
"noaccess": "El registro no existe o no tiene acceso a él.",
"saving": "",
"selectexistingornew": ""
},
"fields": {
@@ -1967,8 +1955,7 @@
"ownr_st": "Provincia del estado",
"ownr_title": "Título",
"ownr_zip": "código postal",
"preferred_contact": "Método de Contacto Preferido",
"tax_number": ""
"preferred_contact": "Método de Contacto Preferido"
},
"forms": {
"address": "",
@@ -2411,7 +2398,6 @@
"open_orders_status": "",
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2424,7 +2410,6 @@
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"psr_by_make": "",
"purchases_by_cost_center_detail": "",
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",
@@ -2449,9 +2434,6 @@
},
"schedule": {
"labels": {
"employeevacation": "",
"intake": "",
"manual": "",
"manualevent": ""
}
},
@@ -2474,8 +2456,6 @@
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"entries": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",

View File

@@ -82,12 +82,8 @@
},
"audit": {
"fields": {
"cc": "",
"contents": "",
"created": "",
"operation": "",
"subject": "",
"to": "",
"useremail": "",
"values": ""
}
@@ -682,7 +678,6 @@
"refuelqty": ""
},
"correctdataonform": "",
"dateinpast": "",
"dlexpirebeforereturn": "",
"driverinformation": "",
"findcontract": "",
@@ -757,7 +752,6 @@
"status": {
"in": "",
"inservice": "",
"leasereturn": "",
"out": "",
"sold": ""
},
@@ -926,8 +920,7 @@
},
"successes": {
"delete": "L'employé a bien été supprimé.",
"save": "L'employé a enregistré avec succès.",
"vacationadded": ""
"save": "L'employé a enregistré avec succès."
},
"validation": {
"unique_employee_number": ""
@@ -1618,10 +1611,8 @@
"documents-images": "",
"documents-other": "",
"duplicateconfirm": "",
"emailaudit": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
"existing_jobs": "Emplois existants",
"federal_tax_amt": "",
"gpdollars": "",
@@ -1925,9 +1916,7 @@
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Ajouter une note...",
"notetoadd": "",
"systemnotes": "",
"usernotes": ""
"notetoadd": ""
},
"successes": {
"create": "Remarque créée avec succès.",
@@ -1946,7 +1935,6 @@
},
"errors": {
"noaccess": "L'enregistrement n'existe pas ou vous n'y avez pas accès.",
"saving": "",
"selectexistingornew": ""
},
"fields": {
@@ -1967,8 +1955,7 @@
"ownr_st": "Etat / Province",
"ownr_title": "Titre",
"ownr_zip": "Zip / code postal",
"preferred_contact": "Méthode de contact préférée",
"tax_number": ""
"preferred_contact": "Méthode de contact préférée"
},
"forms": {
"address": "",
@@ -2411,7 +2398,6 @@
"open_orders_status": "",
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2424,7 +2410,6 @@
"production_by_target_date": "",
"production_by_technician": "",
"production_by_technician_one": "",
"psr_by_make": "",
"purchases_by_cost_center_detail": "",
"purchases_by_cost_center_summary": "",
"purchases_by_date_range_detail": "",
@@ -2449,9 +2434,6 @@
},
"schedule": {
"labels": {
"employeevacation": "",
"intake": "",
"manual": "",
"manualevent": ""
}
},
@@ -2474,8 +2456,6 @@
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"entries": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",

View File

@@ -40,7 +40,6 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
admin_jobmarkexported: () =>
i18n.t("audit_trail.messages.admin_jobmarkexported"),
};
export default AuditTrailMapping;

View File

@@ -1448,19 +1448,6 @@ export const TemplateList = (type, context) => {
},
group: "purchases",
},
parts_not_recieved_vendor: {
title: i18n.t("reportcenter.templates.parts_not_recieved_vendor"),
description: "",
subject: i18n.t("reportcenter.templates.parts_not_recieved_vendor"),
key: "parts_not_recieved_vendor",
idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.parts_orders"),
field: i18n.t("parts_orders.fields.order_date"),
},
group: "purchases",
},
scoreboard_detail: {
title: i18n.t("reportcenter.templates.scoreboard_detail"),
description: "",
@@ -1559,18 +1546,6 @@ export const TemplateList = (type, context) => {
},
group: "jobs",
},
psr_by_make: {
title: i18n.t("reportcenter.templates.psr_by_make"),
subject: i18n.t("reportcenter.templates.psr_by_make"),
key: "psr_by_make",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
}
: {}),
...(!type || type === "courtesycarcontract"

File diff suppressed because it is too large Load Diff

View File

@@ -752,13 +752,6 @@
table:
schema: public
name: documents
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: email_audit_trail
- name: employees
using:
foreign_key_constraint_on:
@@ -1834,93 +1827,6 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: email_audit_trail
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
- name: user
using:
foreign_key_constraint_on: useremail
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
backend_only: false
select_permissions:
- role: user
permission:
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
update_permissions:
- role: user
permission:
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table:
schema: public
name: employee_vacation
@@ -2479,7 +2385,6 @@
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -2544,7 +2449,6 @@
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -2620,7 +2524,6 @@
- bett_type
- cert_part
- convertedtolbr
- convertedtolbr_data
- created_at
- db_hrs
- db_price
@@ -2820,13 +2723,6 @@
table:
schema: public
name: documents
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: email_audit_trail
- name: exportlogs
using:
foreign_key_constraint_on:
@@ -4116,7 +4012,6 @@
- ownr_zip
- preferred_contact
- shopid
- tax_number
- updated_at
filter:
bodyshop:
@@ -4152,7 +4047,6 @@
- ownr_zip
- preferred_contact
- shopid
- tax_number
- updated_at
filter:
bodyshop:
@@ -5077,13 +4971,6 @@
table:
schema: public
name: audit_trail
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: useremail
table:
schema: public
name: email_audit_trail
- name: exportlogs
using:
foreign_key_constraint_on:

View File

@@ -1,4 +0,0 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."joblines" add column "convertedtolbr_data" jsonb
-- not null default jsonb_build_object();

View File

@@ -1,2 +0,0 @@
alter table "public"."joblines" add column "convertedtolbr_data" jsonb
not null default jsonb_build_object();

View File

@@ -1,4 +0,0 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."owners" add column "tax_number" text
-- null;

View File

@@ -1,2 +0,0 @@
alter table "public"."owners" add column "tax_number" text
null;

View File

@@ -1 +0,0 @@
DROP TABLE "public"."email_audit_trail";

View File

@@ -1,18 +0,0 @@
CREATE TABLE "public"."email_audit_trail" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "jobid" uuid, "noteid" uuid, "to" jsonb NOT NULL DEFAULT jsonb_build_array(), "cc" jsonb NOT NULL DEFAULT jsonb_build_array(), "subject" text, "contents" text, "useremail" text NOT NULL, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_email_audit_trail_updated_at"
BEFORE UPDATE ON "public"."email_audit_trail"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_email_audit_trail_updated_at" ON "public"."email_audit_trail"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -1 +0,0 @@
DROP TABLE "public"."email_audit_trail";

View File

@@ -1,18 +0,0 @@
CREATE TABLE "public"."email_audit_trail" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "jobid" uuid, "noteid" uuid, "to" jsonb NOT NULL DEFAULT jsonb_build_array(), "cc" jsonb NOT NULL DEFAULT jsonb_build_array(), "subject" text, "contents" text, "useremail" text NOT NULL, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_email_audit_trail_updated_at"
BEFORE UPDATE ON "public"."email_audit_trail"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_email_audit_trail_updated_at" ON "public"."email_audit_trail"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -1 +0,0 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_bodyshopid_fkey";

View File

@@ -1,5 +0,0 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_bodyshopid_fkey"
foreign key ("bodyshopid")
references "public"."bodyshops"
("id") on update cascade on delete cascade;

View File

@@ -1 +0,0 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_jobid_fkey";

View File

@@ -1,5 +0,0 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_jobid_fkey"
foreign key ("jobid")
references "public"."jobs"
("id") on update cascade on delete cascade;

View File

@@ -1 +0,0 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_useremail_fkey";

View File

@@ -1,5 +0,0 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_useremail_fkey"
foreign key ("useremail")
references "public"."users"
("email") on update cascade on delete cascade;

View File

@@ -17,11 +17,11 @@
"start": "node server.js"
},
"dependencies": {
"aws-sdk": "^2.1181.0",
"aws-sdk": "^2.1136.0",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
"body-parser": "^1.20.0",
"cloudinary": "^1.30.1",
"cloudinary": "^1.30.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
@@ -29,33 +29,35 @@
"dinero.js": "^1.9.1",
"dotenv": "16.0.1",
"express": "^4.18.1",
"firebase-admin": "^11.0.0",
"firebase-admin": "^10.2.0",
"graphql": "^16.5.0",
"graphql-request": "^4.2.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.1",
"inline-css": "^3.0.0",
"intuit-oauth": "^4.0.0",
"json-2-csv": "^3.17.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^5.1.0",
"multer": "^1.4.4",
"node-mailjet": "^3.4.1",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.7.7",
"phone": "^3.1.23",
"nodemailer": "^6.7.5",
"phone": "^3.1.17",
"query-string": "^7.1.1",
"soap": "^0.45.0",
"soap": "^0.43.0",
"socket.io": "^4.5.0",
"ssh2-sftp-client": "^9.0.2",
"stripe": "^9.15.0",
"twilio": "^3.80.0",
"ssh2-sftp-client": "^8.0.0",
"stripe": "^9.1.0",
"twilio": "^3.77.0",
"uuid": "^8.3.2",
"xml2js": "^0.4.23",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {
"concurrently": "^7.3.0",
"concurrently": "^6.3.0",
"eslint": "^7.31.0",
"eslint-plugin-promise": "^5.1.0",
"source-map-explorer": "^2.5.2"
}
}

View File

@@ -172,18 +172,6 @@ app.post(
fb.validateAdmin,
adm.createShop
);
app.post(
"/adm/updateshop",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.updateShop
);
app.post(
"/adm/updatecounter",
fb.validateFirebaseIdToken,
fb.validateAdmin,
adm.updateCounter
);
//Stripe Processing
var stripe = require("./server/stripe/payment");

View File

@@ -1,7 +1,6 @@
const DineroQbFormat = require("./accounting-constants").DineroQbFormat;
const Dinero = require("dinero.js");
const { DiscountNotAlreadyCounted } = require("../job/job-totals");
const logger = require("../utils/logger");
exports.default = function ({
@@ -38,22 +37,23 @@ exports.default = function ({
amount: Math.round((jobline.act_price || 0) * 100),
}).multiply(jobline.part_qty || 1);
// console.log("Have a part discount", jobline);
DineroAmount = DineroAmount.add(
((jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0) ||
(jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(jobline, jobs_by_pk.joblines)
? jobline.prt_dsmk_m
if (
(jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) ||
((jobline.db_ref === "900511" ||
jobline.db_ref === "900510" ||
jobline.db_ref === "900500") &&
jobline.prt_dsmk_m &&
jobline.prt_dsmk_m !== 0)
) {
// console.log("Have a part discount", jobline);
DineroAmount = DineroAmount.add(
jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(jobline.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(jobline.act_price * 100),
})
.multiply(jobline.part_qty || 0)
.percentage(Math.abs(jobline.prt_dsmk_p || 0))
.multiply(jobline.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
: DineroAmount.percentage(
Math.abs(jobline.prt_dsmk_p || 0)
).multiply(jobline.prt_dsmk_p > 0 ? 1 : -1)
);
}
const account = responsibilityCenters.profits.find(
(i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase()
);
@@ -82,11 +82,7 @@ exports.default = function ({
state:
jobs_by_pk.state_tax_rate === 0
? false
: jobline.db_ref === "900511" ||
jobline.db_ref === "900510" ||
(jobline.mod_lb_hrs === 0 && //Extending IO-1375 as a part of IO-2023
jobline.act_price > 0 &&
jobline.lbr_op === "OP14")
: jobline.db_ref === "900511" || jobline.db_ref === "900510"
? true
: jobline.tax_part,
},

View File

@@ -414,11 +414,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
});
setNewRefreshToken(req.user.email, items);
const taxCodes = await oauthClient.makeApiCall({
url: urlBuilder(
qbo_realmId,
"query",
`select * From TaxCode where active=true`
),
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`),
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@@ -66,55 +66,3 @@ exports.createShop = async (req, res) => {
res.status(500).json(error);
}
};
exports.updateCounter = async (req, res) => {
logger.log("admin-update-counter", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
const { id, counter } = req.body;
try {
const result = await client.request(
`mutation UPDATE_COUNTER($id: uuid!, $counter: counters_set_input!) {
update_counters_by_pk(pk_columns: { id: $id }, _set: $counter) {
id
countertype
count
prefix
}
}`,
{
id,
counter,
}
);
res.json(result);
} catch (error) {
res.status(500).json(error);
}
};
exports.updateShop = async (req, res) => {
logger.log("admin-update-shop", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
});
const { id, bodyshop } = req.body;
try {
const result = await client.request(
`mutation UPDATE_BODYSHOP($id: uuid!, $bodyshop: bodyshops_set_input!) {
update_bodyshops_by_pk(pk_columns: { id: $id }, _set: $bodyshop) {
id
}
}`,
{
id,
bodyshop,
}
);
res.json(result);
} catch (error) {
res.status(500).json(error);
}
};

View File

@@ -12,7 +12,6 @@ const CdkBase = require("../web-sockets/web-socket");
const Dinero = require("dinero.js");
const _ = require("lodash");
const { DiscountNotAlreadyCounted } = require("../job/job-totals");
exports.default = async function (socket, jobid) {
try {
@@ -71,20 +70,23 @@ exports.default = async function (socket, jobid) {
amount: Math.round(val.act_price * 100),
}).multiply(val.part_qty || 1);
DineroAmount = DineroAmount.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
if (
(val.prt_dsmk_p && val.prt_dsmk_p !== 0) ||
((val.db_ref === "900511" ||
val.db_ref === "900510" ||
val.db_ref === "900500") &&
val.prt_dsmk_m &&
val.prt_dsmk_m !== 0)
) {
// console.log("Have a part discount", val);
DineroAmount = DineroAmount.add(
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
: DineroAmount.percentage(Math.abs(val.prt_dsmk_p || 0)).multiply(
val.prt_dsmk_p > 0 ? 1 : -1
)
);
}
acc[val.profitcenter_part] =
acc[val.profitcenter_part].add(DineroAmount);

View File

@@ -431,7 +431,7 @@ async function QueryDmsCustomerById(socket, JobData, CustomerId) {
async function QueryDmsCustomerByName(socket, JobData) {
const ownerName = (
JobData.ownr_co_nm && JobData.ownr_co_nm !== ""
JobData.ownr_co_nm
? JobData.ownr_co_nm
: `${JobData.ownr_ln},${JobData.ownr_fn}`
).replace(replaceSpecialRegex, "");
@@ -725,7 +725,7 @@ async function InsertDmsVehicle(socket) {
manufacturer: {},
vehicle: {
deliveryDate: moment()
// .tz(socket.JobData.bodyshop.timezone)
// .tz(socket.JobData.bodyshop.timezone)
.format("YYYYMMDD"),
licensePlateNo: socket.JobData.plate_no,
make: socket.txEnvelope.dms_make,
@@ -854,7 +854,7 @@ async function UpdateDmsVehicle(socket) {
socket.DMSVeh.dealer.inServiceDate ||
socket.txEnvelope.inservicedate
)
// .tz(socket.JobData.bodyshop.timezone)
// .tz(socket.JobData.bodyshop.timezone)
.toISOString(),
}),
},

View File

@@ -57,10 +57,10 @@ exports.default = async (req, res) => {
TransmitDateTime: moment().format(momentFormat), // Omitted from ARMS docs
},
EventInfo: {
AssignmentEvent: {
CreateDateTime:
job.asgn_date && moment(job.asgn_date).format(momentFormat),
},
// AssignmentEvent: {
// CreateDateTime:
// job.asgn_date && moment(job.asgn_date).format(momentFormat),
// },
// EstimateEvent: {
// UploadDateTime: moment().format(momentFormat),
// },
@@ -258,9 +258,9 @@ exports.default = async (req, res) => {
VINNum: job.v_vin,
},
},
License: {
LicensePlateNum: job.plate_no,
},
// License: {
// LicensePlateNum: job.plate_no,
// },
VehicleDesc: {
//ProductionDate: "2009-10",
ModelYear:
@@ -845,7 +845,7 @@ exports.default = async (req, res) => {
const entegralResponse =
await entegralSoapClient.RepairOrderFolderAddRqAsync(
[jobsToPush[0]],
jobsToPush,
function (err, result, rawResponse, soapHeader, rawRequest) {
fs.writeFileSync(`./logs/arms-request.xml`, rawRequest);
fs.writeFileSync(`./logs/arms-response.xml`, rawResponse);
@@ -910,13 +910,10 @@ function GetDocumentstatus(job, bodyshop) {
function GetRepairStatusCode(job) {
return "25";
}
function GetProductionStageCode(job, bodyshop) {
const result = (bodyshop.features?.entegral).find(
(k) => k.status === job.status
);
return result?.code || "33";
if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status))
return "8D";
return "33";
}
function isEmpty(obj) {

View File

@@ -9,9 +9,6 @@ const axios = require("axios");
let nodemailer = require("nodemailer");
let aws = require("aws-sdk");
const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const ses = new aws.SES({
apiVersion: "latest",
@@ -144,11 +141,7 @@ exports.sendEmail = async (req, res) => {
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
});
res.json({
success: true, //response: info
});
@@ -161,12 +154,7 @@ exports.sendEmail = async (req, res) => {
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({ success: false, error: err });
}
}
@@ -178,27 +166,3 @@ async function getImage(imageUrl) {
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
}
async function logEmail(req, email) {
try {
await client.request(queries.INSERT_EMAIL_AUDIT, {
email: {
to: email.to,
cc: email.cc,
subject: email.subject,
bodyshopid: req.body.bodyshopid,
useremail: req.user.email,
contents: req.body.html,
jobid: req.body.jobid,
},
});
} catch (error) {
logger.log("email-log-error", "error", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
}
}

View File

@@ -209,9 +209,6 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
prt_dsmk_p
prt_dsmk_m
tax_part
line_ref
unq_seq
lbr_op
}
}
bodyshops(where: {associations: {active: {_eq: true}}}) {
@@ -405,8 +402,6 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
profitcenter_part
db_ref
prt_dsmk_p
unq_seq
line_ref
}
}
@@ -1117,7 +1112,6 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
joblines(where: { removed: { _eq: false } }) {
id
db_ref
line_ref
unq_seq
line_ind
tax_part
@@ -1226,7 +1220,6 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
id
db_ref
unq_seq
line_ref
line_ind
tax_part
line_desc
@@ -1322,8 +1315,6 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS {
entegral_id
md_responsibility_centers
imexshopid
timezone
features
}
}
`;
@@ -1452,8 +1443,7 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
op_code_desc
profitcenter_labor
profitcenter_part
line_ref
unq_seq
prt_dsmk_p
}
}
}
@@ -1604,10 +1594,3 @@ exports.INSERT_EXPORT_LOG = `
}
}
`;
exports.INSERT_EMAIL_AUDIT = `mutation INSERT_EMAIL_AUDIT($email: email_audit_trail_insert_input!) {
insert_email_audit_trail_one(object: $email) {
id
}
}
`;

View File

@@ -4,7 +4,6 @@ const queries = require("../graphql-client/queries");
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const { DiscountNotAlreadyCounted } = require("./job-totals");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
@@ -219,7 +218,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalLaborGp.getAmount() /
multiSummary.summaryData.totalLaborSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalLaborGppercent
);
@@ -228,7 +227,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalPartsGp.getAmount() /
multiSummary.summaryData.totalPartsSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalPartsGppercent
@@ -238,7 +237,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalAdditionalGp.getAmount() /
multiSummary.summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
multiSummary.summaryData.totalAdditionalGppercentFormatted =
formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent);
@@ -247,7 +246,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.totalSubletGp.getAmount() /
multiSummary.summaryData.totalSubletSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalSubletGppercent
@@ -257,7 +256,7 @@ async function JobCostingMulti(req, res) {
(multiSummary.summaryData.gpdollars.getAmount() /
multiSummary.summaryData.totalSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
multiSummary.summaryData.gppercentFormatted = formatGpPercent(
multiSummary.summaryData.gppercent
@@ -283,7 +282,7 @@ async function JobCostingMulti(req, res) {
(
(c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) *
100
).toFixed(1)
).toFixed(2)
),
};
});
@@ -309,8 +308,7 @@ function GenerateCostingData(job) {
job.bodyshop.md_responsibility_centers.defaults.profits;
const allCenters = _.union(
job.bodyshop.md_responsibility_centers.profits.map((p) => p.name),
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name),
["Unknown"]
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name)
);
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
@@ -332,15 +330,12 @@ function GenerateCostingData(job) {
}
if (val.mod_lbr_ty) {
const laborProfitCenter =
val.profitcenter_labor ||
defaultProfits[val.mod_lbr_ty] ||
"Unknown";
val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "?";
if (laborProfitCenter === "Unknown")
if (laborProfitCenter === "?")
console.log("Unknown type", val.line_desc, val.mod_lbr_ty);
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
const laborAmount = Dinero({
amount: Math.round((job[rateName] || 0) * 100),
}).multiply(val.mod_lb_hrs || 0);
@@ -349,19 +344,6 @@ function GenerateCostingData(job) {
acc.labor[laborProfitCenter] =
acc.labor[laborProfitCenter].add(laborAmount);
if (
val.mod_lb_hrs === 0 &&
val.act_price > 0 &&
val.lbr_op === "OP14"
) {
//Scenario where SGI may pay out hours using a part price.
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty)
);
}
if (val.mod_lbr_ty === "LAR") {
materialsHours.mapaHrs += val.mod_lb_hrs || 0;
}
@@ -377,9 +359,9 @@ function GenerateCostingData(job) {
val.part_type !== "PASL"
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
val.profitcenter_part || defaultProfits[val.part_type] || "?";
if (partsProfitCenter === "Unknown")
if (partsProfitCenter === "?")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
@@ -393,18 +375,14 @@ function GenerateCostingData(job) {
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
@@ -417,9 +395,9 @@ function GenerateCostingData(job) {
(val.part_type === "PAS" || val.part_type === "PASL")
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
val.profitcenter_part || defaultProfits[val.part_type] || "?";
if (partsProfitCenter === "Unknown")
if (partsProfitCenter === "?")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
@@ -433,18 +411,14 @@ function GenerateCostingData(job) {
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.sublet[partsProfitCenter])
acc.sublet[partsProfitCenter] = Dinero();
@@ -459,20 +433,17 @@ function GenerateCostingData(job) {
const partsProfitCenter =
val.profitcenter_part ||
getAdditionalCostCenter(val, defaultProfits) ||
"Unknown";
"?";
if (partsProfitCenter === "Unknown") {
if (partsProfitCenter === "?") {
console.log("Unknown type", val.line_desc, val.part_type);
}
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(val, job.joblines)
? val.prt_dsmk_m
} else {
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 1)
.add(
val.prt_dsmk_m && val.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
@@ -480,13 +451,13 @@ function GenerateCostingData(job) {
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
);
if (!acc.additional[partsProfitCenter])
acc.additional[partsProfitCenter] = Dinero();
acc.additional[partsProfitCenter] =
acc.additional[partsProfitCenter].add(partsAmount);
if (!acc.additional[partsProfitCenter])
acc.additional[partsProfitCenter] = Dinero();
acc.additional[partsProfitCenter] =
acc.additional[partsProfitCenter].add(partsAmount);
}
}
return acc;
@@ -759,9 +730,9 @@ function GenerateCostingData(job) {
.add(sale_sublet);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / Math.abs(totalSales.getAmount())) *
(gpdollars.getAmount() / totalSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
//Push summary data to avoid extra loop.
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
@@ -852,7 +823,7 @@ function GenerateCostingData(job) {
(summaryData.totalLaborGp.getAmount() /
summaryData.totalLaborSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
summaryData.totalLaborGppercentFormatted = formatGpPercent(
summaryData.totalLaborGppercent
);
@@ -864,7 +835,7 @@ function GenerateCostingData(job) {
(summaryData.totalPartsGp.getAmount() /
summaryData.totalPartsSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
summaryData.totalPartsGppercentFormatted = formatGpPercent(
summaryData.totalPartsGppercent
);
@@ -875,7 +846,7 @@ function GenerateCostingData(job) {
(summaryData.totalAdditionalGp.getAmount() /
summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
summaryData.totalAdditionalGppercentFormatted = formatGpPercent(
summaryData.totalAdditionalGppercent
);
@@ -886,7 +857,7 @@ function GenerateCostingData(job) {
(summaryData.totalSubletGp.getAmount() /
summaryData.totalSubletSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
summaryData.totalSubletGppercentFormatted = formatGpPercent(
summaryData.totalSubletGppercent
);
@@ -895,10 +866,9 @@ function GenerateCostingData(job) {
summaryData.totalCost
);
summaryData.gppercent = (
(summaryData.gpdollars.getAmount() /
Math.abs(summaryData.totalSales.getAmount())) *
(summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) *
100
).toFixed(1);
).toFixed(2);
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
else if (!isFinite(summaryData.gppercent))

View File

@@ -362,27 +362,28 @@ function CalculateRatesTotals(ratesList) {
}
function CalculatePartsTotals(jobLines) {
const jl = jobLines.filter((jl) => !jl.removed);
const ret = jl.reduce(
(acc, value) => {
switch (value.part_type) {
case "PAS":
case "PASL":
return {
...acc,
sublets: {
...acc.sublets,
subtotal: acc.sublets.subtotal.add(
Dinero({
amount: Math.round(value.act_price * 100),
})
.multiply(value.part_qty || 0)
.add(
((value.prt_dsmk_m && value.prt_dsmk_m !== 0) ||
(value.prt_dsmk_p && value.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(value, jl)
? value.prt_dsmk_m
const ret = jobLines
.filter((jl) => !jl.removed)
.reduce(
(acc, value) => {
switch (value.part_type) {
case "PAS":
case "PASL":
return {
...acc,
sublets: {
...acc.sublets,
subtotal: acc.sublets.subtotal.add(
Dinero({
amount: Math.round(value.act_price * 100),
})
.multiply(value.part_qty || 0)
.add(
(value.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -390,28 +391,28 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
)
),
},
};
)
),
},
};
default:
if (
!value.part_type &&
value.db_ref !== "900510" &&
value.db_ref !== "900511"
)
return acc;
return {
...acc,
parts: {
...acc.parts,
prt_dsmk_total: acc.parts.prt_dsmk_total.add(
((value.prt_dsmk_m && value.prt_dsmk_m !== 0) ||
(value.prt_dsmk_p && value.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(value, jl)
? value.prt_dsmk_m
default:
if (
!value.part_type &&
value.db_ref !== "900510" &&
value.db_ref !== "900511"
)
return acc;
return {
...acc,
parts: {
...acc.parts,
prt_dsmk_total: acc.parts.prt_dsmk_total.add(
(value.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -419,45 +420,47 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
),
...(value.part_type
? {
list: {
...acc.parts.list,
[value.part_type]:
acc.parts.list[value.part_type] &&
acc.parts.list[value.part_type].total
? {
total: acc.parts.list[value.part_type].total.add(
Dinero({
),
...(value.part_type
? {
list: {
...acc.parts.list,
[value.part_type]:
acc.parts.list[value.part_type] &&
acc.parts.list[value.part_type].total
? {
total: acc.parts.list[
value.part_type
].total.add(
Dinero({
amount: Math.round(
(value.act_price || 0) * 100
),
}).multiply(value.part_qty || 0)
),
}
: {
total: Dinero({
amount: Math.round(
(value.act_price || 0) * 100
),
}).multiply(value.part_qty || 0)
),
}
: {
total: Dinero({
amount: Math.round(
(value.act_price || 0) * 100
),
}).multiply(value.part_qty || 0),
},
},
}
: {}),
subtotal: acc.parts.subtotal
.add(
Dinero({
amount: Math.round(value.act_price * 100),
}).multiply(value.part_qty || 0)
)
.add(
((value.prt_dsmk_m && value.prt_dsmk_m !== 0) ||
(value.prt_dsmk_p && value.prt_dsmk_p !== 0)) &&
DiscountNotAlreadyCounted(value, jl)
? value.prt_dsmk_m
}).multiply(value.part_qty || 0),
},
},
}
: {}),
subtotal: acc.parts.subtotal
.add(
Dinero({
amount: Math.round(value.act_price * 100),
}).multiply(value.part_qty || 0)
)
.add(
(value.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -465,26 +468,25 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
),
},
};
}
},
{
parts: {
list: {},
prt_dsmk_total: Dinero(),
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
),
},
};
}
},
sublets: {
subtotal: Dinero({ amount: 0 }),
{
parts: {
list: {},
prt_dsmk_total: Dinero(),
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
sublets: {
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
}
);
total: Dinero({ amount: 0 }),
},
}
);
return {
parts: {
@@ -704,16 +706,7 @@ function CalculateTaxesTotals(job, otherTotals) {
exports.default = Totals;
function DiscountNotAlreadyCounted(jobline, joblines) {
if (
//If it's not a discount line, then it definitely hasn't been counted yet.
jobline.db_ref !== "900510" &&
jobline.db_ref !== "900511"
)
return true;
if (jobline.db_ref !== "900510") return true;
const ParentLine = joblines.find((j) => j.unq_seq === jobline.line_ref);
return ParentLine && !(ParentLine.prt_dsmk_m && ParentLine.prt_dsmk_m !== 0);
}
exports.DiscountNotAlreadyCounted = DiscountNotAlreadyCounted;

6949
yarn.lock

File diff suppressed because it is too large Load Diff