Compare commits

...

30 Commits

Author SHA1 Message Date
Patrick Fic
62d5c17de2 IO-2027 LMS Texting & Emails. 2022-08-18 14:46:40 -07:00
Patrick Fic
87c934c886 Add RBAC to tech jobs list. 2022-08-18 10:48:51 -07:00
Patrick Fic
e4b736d4e9 Resolve label typo. 2022-08-17 14:19:28 -07:00
Patrick Fic
d0673bfcba Updated third party notices. 2022-08-17 14:19:21 -07:00
Patrick Fic
fe6e85e993 IO-2023 Resolve hours paid as part for MPI. 2022-08-16 16:18:50 -07:00
Patrick Fic
b744720efe IO-2009 Better handling of MPI discounts. 2022-08-16 14:29:28 -07:00
Patrick Fic
d5c27fc9ae IO-2015 Job Costing fix for lines with no part type but a dollar amount. 2022-08-15 12:39:09 -07:00
Patrick Fic
129c94f066 Check for blank string on owner name 2022-08-15 10:45:19 -07:00
Patrick Fic
503e901295 Merged in release/2022-08-05 (pull request #559)
IO-2008 Split local media into images and other as per cloudinary for better other support.
2022-08-04 16:06:52 +00:00
Patrick Fic
85caf828ea IO-2008 Split local media into images and other as per cloudinary for better other support. 2022-08-03 13:27:45 -07:00
Patrick Fic
92b89af1c7 Merged in release/2022-08-05 (pull request #555)
Add null check to owner display.

Approved-by: Patrick Fic
2022-08-02 18:19:26 +00:00
Patrick Fic
b03d729af6 Add null check to owner display. 2022-08-02 11:18:36 -07:00
Patrick Fic
5c22ce188b Merged in release/2022-08-05 (pull request #554)
Updated antd package to fix drawer issues.

Approved-by: Patrick Fic
2022-08-02 18:11:31 +00:00
Patrick Fic
faef05a95b Updated antd package to fix drawer issues. 2022-08-02 11:11:01 -07:00
Patrick Fic
64aa8336b3 Merged in release/2022-07-29 (pull request #553)
Resolve production board loading issue for MAXWR.

Approved-by: Patrick Fic
2022-07-30 21:42:56 +00:00
Patrick Fic
8700e9a9ae Resolve production board loading issue for MAXWR. 2022-07-30 14:42:31 -07:00
Patrick Fic
d92f706d05 Merged in release/2022-07-29 (pull request #552)
release/2022-07-29

Approved-by: Patrick Fic
2022-07-29 23:05:08 +00:00
Patrick Fic
a62fd5d9b2 IO-1861 Scoreboard entry edit bug fixes. 2022-07-29 10:57:55 -07:00
Patrick Fic
9fec6bbe0c IO-2004 Include adjustments made to new labor types. 2022-07-29 10:37:48 -07:00
Patrick Fic
e379ecfba6 IO-2002 Keep manual lines on supplement. 2022-07-27 13:09:33 -07:00
Patrick Fic
4ef68a0e26 IO-2001 Always expose production note. 2022-07-27 10:00:57 -07:00
Patrick Fic
f36926f228 IO-2000 Add parts_not_recieved_vendor to report center. 2022-07-26 14:30:01 -07:00
Patrick Fic
22cd7fcef1 IO-1861 All scoreboard entries. 2022-07-26 11:22:15 -07:00
Patrick Fic
2586393b4c Added admin functionality. 2022-07-26 09:33:08 -07:00
Patrick Fic
0b448e043c IO-1990 Add alt_partno to bill posting screen. 2022-07-25 12:37:56 -07:00
Patrick Fic
f89e39759a Package updates. 2022-07-25 12:24:35 -07:00
Patrick Fic
ce783fdc5a Merged in release/2022-07-29 (pull request #546)
release/2022-07-29

Approved-by: Patrick Fic
2022-07-25 18:15:43 +00:00
Patrick Fic
8aa99415b1 IO-1995 remove syncing of shop status between tabs to reduce unintended consequences. 2022-07-25 11:14:45 -07:00
Patrick Fic
2ede54b901 IO-1994 Resolve parts filtering status and sorting. 2022-07-25 11:04:03 -07:00
Patrick Fic
19329e76b7 Merged in release/2022-07-22 (pull request #545)
Release/2022 07 22
2022-07-22 17:56:34 +00:00
43 changed files with 20381 additions and 14805 deletions

View File

@@ -19,3 +19,6 @@ 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

@@ -40555,6 +40555,27 @@
</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>
@@ -41535,6 +41556,27 @@
</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.6",
"@apollo/client": "^3.6.9",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3",
"@craco/craco": "^6.4.5",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@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",
"@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",
"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.8.2",
"firebase": "^9.9.1",
"graphql": "^16.5.0",
"i18next": "^21.8.9",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.8.0",
"jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.6",
"logrocket": "^3.0.0",
"markerjs2": "^2.21.4",
"libphonenumber-js": "^1.10.9",
"logrocket": "^3.0.1",
"markerjs2": "^2.22.0",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"normalize-url": "^7.0.3",
"phone": "^3.1.20",
"phone": "^3.1.23",
"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": "^0.40.1",
"react-big-calendar": "^1.5.0",
"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.17.0",
"react-i18next": "^11.18.1",
"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.10",
"recharts": "^2.1.12",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"redux-state-sync": "^3.1.4",
"reselect": "^4.1.6",
"sass": "^1.51.0",
"sass": "^1.54.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.18.9",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.6.1",
"@sentry/webpack-plugin": "^1.19.0",
"@testing-library/cypress": "^8.0.3",
"cypress": "^10.3.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

@@ -27,6 +27,10 @@ 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()))
);
@@ -48,14 +52,17 @@ 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}` : ""
}`}</span>
<span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
</span>
<span style={{ float: "right", paddingleft: "1rem" }}>
{item.act_price
? `$${item.act_price && item.act_price.toFixed(2)}`

View File

@@ -9,6 +9,7 @@ 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({
@@ -58,17 +59,24 @@ export function ChatMediaSelector({
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{data && (
{!bodyshop.uselocalmediaserver && data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && visible && (
<JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={
conversation.job_conversations[0] &&
conversation.job_conversations[0].jobid
}
/>
)}
</div>
);
if (bodyshop.uselocalmediaserver) return null;
return (
<Popover
content={

View File

@@ -5,12 +5,15 @@ 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) => ({
@@ -25,6 +28,7 @@ export function EmailDocumentsComponent({
emailConfig,
form,
selectedMediaState,
bodyshop,
}) {
const { t } = useTranslation();
@@ -52,12 +56,18 @@ export function EmailDocumentsComponent({
10485760 - new Blob([form.getFieldValue("html")]).size ? (
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
) : null}
{data && (
{!bodyshop.uselocalmediaserver && data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{bodyshop.uselocalmediaserver && (
<JobsDocumentsLocalGalleryExternalComponent
externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={emailConfig.jobid}
/>
)}
</div>
);
}

View File

@@ -160,14 +160,13 @@ export function EmailOverlayComponent({
</Form.Item>
<Tabs>
{!bodyshop.uselocalmediaserver && (
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent
selectedMediaState={selectedMediaState}
form={form}
/>
</Tabs.TabPane>
)}
<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

@@ -144,11 +144,26 @@ export function JobLinesComponent({
filters: [
{
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"],
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
@@ -158,9 +173,29 @@ 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", "PASL"],
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"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
@@ -346,7 +381,11 @@ export function JobLinesComponent({
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
};
const handleMark = (e) => {
@@ -482,9 +521,10 @@ export function JobLinesComponent({
</Button>
<Button
onClick={() => {
setState({
setState((state) => ({
...state,
filteredInfo: {
...state.filteredInfo,
part_type: [
"PAN",
"PAC",
@@ -498,7 +538,7 @@ export function JobLinesComponent({
"PAG",
],
},
});
}));
}}
>
<FilterFilled /> {t("jobs.actions.filterpartsonly")}

View File

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

View File

@@ -45,9 +45,11 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
return acc + generateUpdateQuery(value, idx);
}, "");
const removeQueries = existingLines.reduce((acc, value, idx) => {
return acc + generateRemoveQuery(value, idx);
}, "");
const removeQueries = existingLines
.filter((l) => !l.manual_line)
.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,10 +1,10 @@
import { Card, Col, Row, Space, Tag } from "antd";
import {
WarningFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
WarningFilled,
} from "@ant-design/icons";
import React, { useMemo } from "react";
import { Card, Col, Row, Space, Tag } from "antd";
import React 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 ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.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 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";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -56,12 +56,6 @@ 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();
@@ -129,11 +123,10 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel>
)}
{(job.inproduction || jobInPostProduction) && (
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<Space>
{job.special_coverage_policy && (
<Tag color="tomato">

View File

@@ -1,4 +1,4 @@
import { SyncOutlined } from "@ant-design/icons";
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
@@ -58,6 +58,24 @@ 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>
@@ -90,7 +108,7 @@ export function JobsDocumentsLocalGallery({
</Card>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={(allMedia && allMedia[job.id]) || []}
images={jobMedia.images}
backdropClosesModal={true}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
@@ -104,6 +122,31 @@ 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

@@ -0,0 +1,79 @@
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

@@ -6,12 +6,17 @@ 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 allCodes = [...jobCodes, ...ticketCodes].filter(
const adjustmentCodes = Object.keys(adjustments);
const allCodes = [...jobCodes, ...ticketCodes, ...adjustmentCodes].filter(
(value, index, self) => self.indexOf(value) === index && !!value
);

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

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 ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
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";
export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
visible: false,
search: "",
current: 1,
pageSize: 10,
});
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 { 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 columns = [
{
@@ -97,35 +97,69 @@ 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 (
<Dropdown trigger={["click"]} overlay={overlay}>
<Button>Jobs</Button>
</Dropdown>
<>
<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>
</>
);
}

View File

@@ -17,6 +17,8 @@ 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

@@ -185,8 +185,8 @@ export default function ScoreboardTimeTickets() {
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);
ret.employees[ticket.employee.employee_number]
.actualTotalOverPeriod + (ticket.actualhrs || 0);
if (!totals.employees[ticket.employee.employee_number])
totals.employees[ticket.employee.employee_number] = {
@@ -236,8 +236,6 @@ export default function ScoreboardTimeTickets() {
};
}, [fixedPeriods, data, startDate, endDate]);
console.log(calculatedData);
if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
return (

View File

@@ -23,6 +23,7 @@ export const GET_ALL_JOBLINES_BY_PK = gql`
notes
location
tax_part
manual_line
}
}
`;
@@ -197,6 +198,7 @@ export const GET_JOB_LINES_TO_ENTER_BILL = gql`
line_desc
part_type
oem_partno
alt_partno
db_price
act_price
part_qty
@@ -206,6 +208,7 @@ 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

@@ -88,3 +88,41 @@ 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

@@ -1,12 +1,15 @@
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>
<TechLookupJobsList />
<TechLookupJobsDrawer />
<RbacWrapperComponent action="jobs:list-active">
<TechLookupJobsList />
<TechLookupJobsDrawer />
</RbacWrapperComponent>
</div>
);
}

View File

@@ -17,7 +17,9 @@ 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

@@ -2406,6 +2406,7 @@
"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",
@@ -2465,6 +2466,7 @@
"dailyactual": "Actual (D)",
"dailytarget": "Daily",
"efficiencyoverperiod": "Efficiency over Selected Dates",
"entries": "Scoreboard Entries",
"jobs": "Jobs",
"lastmonth": "Last Month",
"lastweek": "Last Week",

View File

@@ -2406,6 +2406,7 @@
"open_orders_status": "",
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2465,6 +2466,7 @@
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"entries": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",

View File

@@ -2406,6 +2406,7 @@
"open_orders_status": "",
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2465,6 +2466,7 @@
"dailyactual": "",
"dailytarget": "",
"efficiencyoverperiod": "",
"entries": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",

View File

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

View File

@@ -1448,6 +1448,19 @@ 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: "",

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -172,6 +172,18 @@ 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,6 +1,7 @@
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 ({
@@ -37,23 +38,22 @@ exports.default = function ({
amount: Math.round((jobline.act_price || 0) * 100),
}).multiply(jobline.part_qty || 1);
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
// 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
? Dinero({ amount: Math.round(jobline.prt_dsmk_m * 100) })
: DineroAmount.percentage(
Math.abs(jobline.prt_dsmk_p || 0)
).multiply(jobline.prt_dsmk_p > 0 ? 1 : -1)
);
}
: 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()
);
const account = responsibilityCenters.profits.find(
(i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase()
);
@@ -82,7 +82,11 @@ exports.default = function ({
state:
jobs_by_pk.state_tax_rate === 0
? false
: jobline.db_ref === "900511" || jobline.db_ref === "900510"
: 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")
? true
: jobline.tax_part,
},

View File

@@ -66,3 +66,55 @@ 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,6 +12,7 @@ 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 {
@@ -70,23 +71,20 @@ exports.default = async function (socket, jobid) {
amount: Math.round(val.act_price * 100),
}).multiply(val.part_qty || 1);
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
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
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: DineroAmount.percentage(Math.abs(val.prt_dsmk_p || 0)).multiply(
val.prt_dsmk_p > 0 ? 1 : -1
)
);
}
: 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()
);
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

@@ -209,6 +209,9 @@ 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}}}) {
@@ -402,6 +405,8 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
profitcenter_part
db_ref
prt_dsmk_p
unq_seq
line_ref
}
}
@@ -1112,6 +1117,7 @@ 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
@@ -1220,6 +1226,7 @@ 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
@@ -1443,7 +1450,8 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
line_ref
unq_seq
}
}
}

View File

@@ -4,6 +4,7 @@ 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";
@@ -308,7 +309,8 @@ 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)
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name),
["Unknown"]
);
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
@@ -330,12 +332,15 @@ function GenerateCostingData(job) {
}
if (val.mod_lbr_ty) {
const laborProfitCenter =
val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "?";
val.profitcenter_labor ||
defaultProfits[val.mod_lbr_ty] ||
"Unknown";
if (laborProfitCenter === "?")
if (laborProfitCenter === "Unknown")
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);
@@ -344,6 +349,19 @@ 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;
}
@@ -359,9 +377,9 @@ function GenerateCostingData(job) {
val.part_type !== "PASL"
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "?";
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "?")
if (partsProfitCenter === "Unknown")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
@@ -375,14 +393,18 @@ function GenerateCostingData(job) {
})
.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),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
((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()
);
if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
@@ -395,9 +417,9 @@ function GenerateCostingData(job) {
(val.part_type === "PAS" || val.part_type === "PASL")
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "?";
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "?")
if (partsProfitCenter === "Unknown")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
@@ -411,14 +433,18 @@ function GenerateCostingData(job) {
})
.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),
})
.multiply(val.part_qty || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
((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()
);
if (!acc.sublet[partsProfitCenter])
acc.sublet[partsProfitCenter] = Dinero();
@@ -433,17 +459,20 @@ function GenerateCostingData(job) {
const partsProfitCenter =
val.profitcenter_part ||
getAdditionalCostCenter(val, defaultProfits) ||
"?";
"Unknown";
if (partsProfitCenter === "?") {
if (partsProfitCenter === "Unknown") {
console.log("Unknown type", val.line_desc, val.part_type);
} 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
}
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
? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(val.act_price * 100),
@@ -451,13 +480,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;

View File

@@ -362,28 +362,27 @@ function CalculateRatesTotals(ratesList) {
}
function CalculatePartsTotals(jobLines) {
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
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
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -391,28 +390,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.db_ref === "900511" ||
value.db_ref === "900510" ||
value.db_ref === "900500") &&
value.prt_dsmk_m &&
value.prt_dsmk_m !== 0
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
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -420,47 +419,45 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
),
...(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({
: 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),
},
},
}
: {}),
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
}).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
? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) })
: Dinero({
amount: Math.round(value.act_price * 100),
@@ -468,25 +465,26 @@ function CalculatePartsTotals(jobLines) {
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
),
},
};
}
},
{
parts: {
list: {},
prt_dsmk_total: Dinero(),
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
sublets: {
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
: Dinero()
),
},
};
}
);
},
{
parts: {
list: {},
prt_dsmk_total: Dinero(),
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
sublets: {
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
}
);
return {
parts: {
@@ -706,7 +704,16 @@ function CalculateTaxesTotals(job, otherTotals) {
exports.default = Totals;
function DiscountNotAlreadyCounted(jobline, joblines) {
if (jobline.db_ref !== "900510") return true;
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;
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;

2711
yarn.lock

File diff suppressed because it is too large Load Diff