Merged in hotfix/2020-05-28 (pull request #72)

Hotfix/2020 05 28 deployment to Test.
This commit is contained in:
Patrick Fic
2021-05-27 21:59:43 +00:00
39 changed files with 914 additions and 102 deletions

View File

@@ -3339,6 +3339,53 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<folder_node>
<name>jc_hourly_rates</name>
<children>
<concept_node>
<name>mapa</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>mash</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>
<concept_node> <concept_node>
<name>lastnumberworkingdays</name> <name>lastnumberworkingdays</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -10757,6 +10804,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>updating</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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -12947,6 +13015,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>nointernet</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>nointernet_sub</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>none</name> <name>none</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -22179,6 +22289,27 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<concept_node>
<name>prt_dsmk_total</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>rates</name> <name>rates</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -27859,6 +27990,48 @@
<folder_node> <folder_node>
<name>labels</name> <name>labels</name>
<children> <children>
<concept_node>
<name>noneselected</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>onenamerequired</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>vendorcategory</name> <name>vendorcategory</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -30292,6 +30465,32 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>updated</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>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -14,6 +14,7 @@ if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
export default function AppContainer() { export default function AppContainer() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider <ConfigProvider

View File

@@ -1,3 +1,4 @@
import { Button, Result } from "antd";
import React, { lazy, Suspense, useEffect } from "react"; import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -8,6 +9,8 @@ import ErrorBoundary from "../components/error-boundary/error-boundary.component
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import AboutPage from "../pages/about/about.page"; import AboutPage from "../pages/about/about.page";
import TechPageContainer from "../pages/tech/tech.page.container"; import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions"; import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors"; import { selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route"; import PrivateRoute from "../utils/private-route";
@@ -27,12 +30,14 @@ const MobilePaymentContainer = lazy(() =>
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
online: selectOnline,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()), checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ checkUserSession, currentUser }) { export function App({ checkUserSession, currentUser, online, setOnline }) {
useEffect(() => { useEffect(() => {
checkUserSession(); checkUserSession();
}, [checkUserSession]); }, [checkUserSession]);
@@ -42,10 +47,38 @@ export function App({ checkUserSession, currentUser }) {
const { t } = useTranslation(); const { t } = useTranslation();
window.addEventListener("offline", function (e) {
console.log("Internet connection lost.");
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
if (currentUser.authorized === null) { if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />; return <LoadingSpinner message={t("general.labels.loggingin")} />;
} }
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
return ( return (
<Switch> <Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}> <Suspense fallback={<LoadingSpinner message="ImEX Online" />}>

View File

@@ -128,7 +128,7 @@ export const uploadToCloudinary = async (
let takenat; let takenat;
if (fileType.includes("image")) { if (fileType.includes("image")) {
const exif = await exifr.parse(file); const exif = await exifr.parse(file);
console.log(`exif`, exif);
takenat = exif && exif.DateTimeOriginal; takenat = exif && exif.DateTimeOriginal;
} }
const documentInsert = await client.mutate({ const documentInsert = await client.mutate({
@@ -141,7 +141,7 @@ export const uploadToCloudinary = async (
uploaded_by: uploaded_by, uploaded_by: uploaded_by,
key: key, key: key,
type: fileType, type: fileType,
extension: extension, extension: cloudinaryUploadResponse.data.format || extension,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
size: cloudinaryUploadResponse.data.bytes || file.size, size: cloudinaryUploadResponse.data.bytes || file.size,
takenat, takenat,
@@ -176,6 +176,7 @@ export const uploadToCloudinary = async (
} }
}; };
//Also needs to be updated in media JS and mobile app.
export function DetermineFileType(filetype) { export function DetermineFileType(filetype) {
if (!filetype) return "auto"; if (!filetype) return "auto";
else if (filetype.startsWith("image")) return "image"; else if (filetype.startsWith("image")) return "image";

View File

@@ -155,7 +155,14 @@ export function JobLinesComponent({
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter> <>
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
{record.prt_dsmk_p !== 0 && (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
)}
</>
), ),
}, },
{ {

View File

@@ -69,17 +69,28 @@ export default function JobTotalsTableParts({ job }) {
x: true, x: true,
}} }}
summary={() => ( summary={() => (
<Table.Summary.Row> <>
<Table.Summary.Cell> <Table.Summary.Row>
<strong>{t("jobs.labels.partstotal")}</strong> <Table.Summary.Cell>
</Table.Summary.Cell> {t("jobs.labels.prt_dsmk_total")}
</Table.Summary.Cell>
<Table.Summary.Cell align="right"> <Table.Summary.Cell align="right">
<strong> {Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
{Dinero(job.job_totals.parts.parts.total).toFormat()} </Table.Summary.Cell>
</strong> </Table.Summary.Row>
</Table.Summary.Cell> <Table.Summary.Row>
</Table.Summary.Row> <Table.Summary.Cell>
<strong>{t("jobs.labels.partstotal")}</strong>
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<strong>
{Dinero(job.job_totals.parts.parts.total).toFormat()}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
)} )}
/> />
); );

View File

@@ -32,7 +32,7 @@ mutation UNVOID_JOB($jobId: uuid!) {
} }
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${ insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
currentUser.email currentUser.email
}", text: "${t("jobs.labels.unvoidnote", { email: currentUser.email })}"}) { }", text: "${t("jobs.labels.unvoidnote")}"}) {
returning { returning {
id id
} }

View File

@@ -124,10 +124,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
data: { data: {
created_by: currentUser.email, created_by: currentUser.email,
audit: true, audit: true,
text: t("jobs.labels.importnote", { text: t("jobs.labels.importnote"),
date: moment().format("MM/DD/yyy"),
time: moment().format("hh:mm a"),
}),
}, },
}, },
queued_for_parts: true, queued_for_parts: true,
@@ -278,10 +275,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
jobid: selectedJob, jobid: selectedJob,
created_by: currentUser.email, created_by: currentUser.email,
audit: true, audit: true,
text: t("jobs.labels.supplementnote", { text: t("jobs.labels.supplementnote"),
date: moment().format("MM/DD/yyy"),
time: moment().format("hh:mm a"),
}),
}, },
], ],
}, },

View File

@@ -1,7 +1,6 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd"; import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
import moment from "moment";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -385,10 +384,7 @@ export function JobsDetailHeaderActions({
jobid: job.id, jobid: job.id,
created_by: currentUser.email, created_by: currentUser.email,
audit: true, audit: true,
text: t("jobs.labels.voidnote", { text: t("jobs.labels.voidnote"),
date: moment().format("MM/DD/yyy"),
time: moment().format("hh:mm a"),
}),
}, },
], ],
}, },

View File

@@ -151,17 +151,21 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<Card <Card
style={{ height: "100%" }} style={{ height: "100%" }}
title={ title={
<Link job.vehicle ? (
to={ <Link
disabled to={
? "#" disabled
: job.vehicle && `/manage/vehicles/${job.vehicle.id}` ? "#"
} : job.vehicle && `/manage/vehicles/${job.vehicle.id}`
> }
{`${job.v_model_yr || ""} ${job.v_color || ""} >
{`${job.v_model_yr || ""} ${job.v_color || ""}
${job.v_make_desc || ""} ${job.v_make_desc || ""}
${job.v_model_desc || ""}`} ${job.v_model_desc || ""}`}
</Link> </Link>
) : (
<span></span>
)
} }
> >
<div> <div>

View File

@@ -31,7 +31,9 @@ function JobsDocumentsComponent({
acc.images.push({ acc.images.push({
src: `${ src: `${
process.env.REACT_APP_CLOUDINARY_ENDPOINT process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${DetermineFileType(value.type)}/upload/${value.key}`, }/${DetermineFileType(value.type)}/upload/${value.key}${
value.extension ? `.${value.extension}` : ""
}`,
thumbnail: `${ thumbnail: `${
process.env.REACT_APP_CLOUDINARY_ENDPOINT process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${DetermineFileType(value.type)}/upload/${ }/${DetermineFileType(value.type)}/upload/${
@@ -51,13 +53,17 @@ function JobsDocumentsComponent({
let thumb; let thumb;
switch (fileType) { switch (fileType) {
case "video": case "video":
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/c_fill,f_png,h_250,w_250/${value.key}`; thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}`;
break; break;
case "raw": case "raw":
thumb = `${window.location.origin}/file.png`; thumb = `${window.location.origin}/file.png`;
break; break;
default: default:
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}`; thumb = `${
process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${fileType}/upload/${
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
}/${value.key}${value.extension ? `.${value.extension}` : ""}`;
break; break;
} }
@@ -65,9 +71,9 @@ function JobsDocumentsComponent({
acc.other.push({ acc.other.push({
src: `${ src: `${
process.env.REACT_APP_CLOUDINARY_ENDPOINT process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${fileType}/upload/${fileType === "video" ? "q_auto/" : ""}${ }/${fileType}/upload/${value.key}${
value.key value.extension ? `.${value.extension}` : ""
}${fileType === "raw" ? `.${value.extension}` : ""}`, }`,
thumbnail: thumb, thumbnail: thumb,
tags: [ tags: [
{ {

View File

@@ -82,7 +82,31 @@ export function PhonebookFormComponent({
<Form.Item label={t("phonebook.fields.lastname")} name="lastname"> <Form.Item label={t("phonebook.fields.lastname")} name="lastname">
<Input disabled={hasNoAccess} /> <Input disabled={hasNoAccess} />
</Form.Item> </Form.Item>
<Form.Item label={t("phonebook.fields.company")} name="company"> <Form.Item
label={t("phonebook.fields.company")}
name="company"
dependencies={["firstname", "lastname"]}
rules={[
({ getFieldsValue }) => ({
validator(rule, value) {
const { firstname, lastname, company } = getFieldsValue([
"firstname",
"lastname",
"company",
]);
if (
(firstname && firstname.trim() !== "") ||
(lastname && lastname.trim() !== "") ||
(company && company.trim() !== "")
) {
return Promise.resolve();
}
return Promise.reject(t("phonebook.labels.onenamerequired"));
},
}),
]}
>
<Input disabled={hasNoAccess} /> <Input disabled={hasNoAccess} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>

View File

@@ -16,7 +16,7 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
export default function ShopInfoGeneral({ form }) { export default function ShopInfoGeneral({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -425,6 +425,18 @@ export default function ShopInfoGeneral({ form }) {
> >
<InputNumber min={0} max={1} precision={2} /> <InputNumber min={0} max={1} precision={2} />
</Form.Item> </Form.Item>
<Form.Item
label={t("bodyshop.fields.jc_hourly_rates.mapa")}
name={["jc_hourly_rates", "mapa"]}
>
<CurrencyInput />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.jc_hourly_rates.mash")}
name={["jc_hourly_rates", "mash"]}
>
<CurrencyInput />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}> <LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}>
<Form.List name={["md_messaging_presets"]}> <Form.List name={["md_messaging_presets"]}>

View File

@@ -87,6 +87,7 @@ export const QUERY_BODYSHOP = gql`
md_ccc_rates md_ccc_rates
enforce_referral enforce_referral
website website
jc_hourly_rates
employees { employees {
id id
active active
@@ -171,6 +172,7 @@ export const UPDATE_SHOP = gql`
md_ccc_rates md_ccc_rates
enforce_referral enforce_referral
website website
jc_hourly_rates
employees { employees {
id id
first_name first_name

View File

@@ -20,6 +20,7 @@ export const GET_DOCUMENTS_BY_JOB = gql`
type type
size size
takenat takenat
extension
bill { bill {
id id
invoice_number invoice_number

View File

@@ -528,6 +528,7 @@ export const GET_JOB_BY_PK = gql`
tax_part tax_part
db_ref db_ref
manual_line manual_line
prt_dsmk_p
billlines(limit: 1, order_by: { bill: { date: desc } }) { billlines(limit: 1, order_by: { bill: { date: desc } }) {
id id
quantity quantity

View File

@@ -385,7 +385,7 @@ export function Manage({ match, conflict, bodyshop }) {
<Footer> <Footer>
<div style={{ textAlign: "center", margin: "1rem 0rem" }}> <div style={{ textAlign: "center", margin: "1rem 0rem" }}>
<div> <div>
{`ImEX Online V.${process.env.NODE_ENV} - ${ {`ImEX Online ${
process.env.REACT_APP_GIT_SHA process.env.REACT_APP_GIT_SHA
} - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`} } - ${preval`module.exports = new Date().toLocaleString("en-US", {timeZone: "America/Los_Angeles"});`}`}
</div> </div>

View File

@@ -48,3 +48,8 @@ export const setPartnerVersion = (version) => ({
type: ApplicationActionTypes.SET_PARTNER_VERSION, type: ApplicationActionTypes.SET_PARTNER_VERSION,
payload: version, payload: version,
}); });
export const setOnline = (isOnline) => ({
type: ApplicationActionTypes.SET_ONLINE_STATUS,
payload: isOnline,
});

View File

@@ -2,6 +2,7 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = { const INITIAL_STATE = {
loading: false, loading: false,
online: true,
breadcrumbs: [], breadcrumbs: [],
recentItems: [], recentItems: [],
selectedHeader: "home", selectedHeader: "home",
@@ -21,6 +22,11 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
...state, ...state,
selectedHeader: action.payload, selectedHeader: action.payload,
}; };
case ApplicationActionTypes.SET_ONLINE_STATUS:
return {
...state,
online: action.payload,
};
case ApplicationActionTypes.ADD_RECENT_ITEM: case ApplicationActionTypes.ADD_RECENT_ITEM:
return { return {
...state, ...state,

View File

@@ -40,3 +40,7 @@ export const selectJobReadOnly = createSelector(
[selectApplication], [selectApplication],
(application) => application.jobReadOnly (application) => application.jobReadOnly
); );
export const selectOnline = createSelector(
[selectApplication],
(application) => application.online
);

View File

@@ -9,5 +9,6 @@ const ApplicationActionTypes = {
SET_SELECTED_HEADER: "SET_SELECTED_HEADER", SET_SELECTED_HEADER: "SET_SELECTED_HEADER",
SET_JOB_READONLY: "SET_JOB_READONLY", SET_JOB_READONLY: "SET_JOB_READONLY",
SET_PARTNER_VERSION: "SET_PARTNER_VERSION", SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
}; };
export default ApplicationActionTypes; export default ApplicationActionTypes;

View File

@@ -1,4 +1,6 @@
import Fingerprint2 from "@fingerprintjs/fingerprintjs"; import Fingerprint2 from "@fingerprintjs/fingerprintjs";
import { notification } from "antd";
import i18next from "i18next";
import LogRocket from "logrocket"; import LogRocket from "logrocket";
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects"; import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
import { import {
@@ -100,6 +102,10 @@ export function* updateUserDetails(userDetails) {
try { try {
yield updateCurrentUser(userDetails.payload); yield updateCurrentUser(userDetails.payload);
yield put(updateUserDetailsSuccess(userDetails.payload)); yield put(updateUserDetailsSuccess(userDetails.payload));
notification.open({
type: "success",
message: i18next.t("profile.successes.updated"),
});
} catch (error) { } catch (error) {
//yield put(signOutFailure(error.message)); //yield put(signOutFailure(error.message));
} }

View File

@@ -218,6 +218,10 @@
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate", "invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
"invoice_local_tax_rate": "Invoices - Local Tax Rate", "invoice_local_tax_rate": "Invoices - Local Tax Rate",
"invoice_state_tax_rate": "Invoices - Provincial/State Tax Rate", "invoice_state_tax_rate": "Invoices - Provincial/State Tax Rate",
"jc_hourly_rates": {
"mapa": "Job Costing - Paint Materials Hourly Cost Rate",
"mash": "Job Costing - Shop Materials Hourly Cost Rate"
},
"lastnumberworkingdays": "Scoreboard - Last Number of Working Days", "lastnumberworkingdays": "Scoreboard - Last Number of Working Days",
"logo_img_path": "Shop Logo", "logo_img_path": "Shop Logo",
"logo_img_path_height": "Logo Image Height", "logo_img_path_height": "Logo Image Height",
@@ -555,7 +559,7 @@
"driverinformation": "Driver's Information", "driverinformation": "Driver's Information",
"findcontract": "Find Contract", "findcontract": "Find Contract",
"findermodal": "Contract Finder", "findermodal": "Contract Finder",
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job", "populatefromjob": "Populate from Job",
"rates": "Contract Rates", "rates": "Contract Rates",
"time": "Time", "time": "Time",
@@ -683,7 +687,8 @@
"deleting_cloudinary": "Error deleting document from storage. {{message}}", "deleting_cloudinary": "Error deleting document from storage. {{message}}",
"getpresignurl": "Error obtaining presigned URL for document. {{message}}", "getpresignurl": "Error obtaining presigned URL for document. {{message}}",
"insert": "Unable to upload file. {{message}}", "insert": "Unable to upload file. {{message}}",
"nodocuments": "There are no documents." "nodocuments": "There are no documents.",
"updating": "Error updating document. {{error}}"
}, },
"labels": { "labels": {
"confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.", "confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.",
@@ -820,6 +825,8 @@
"monday": "Monday", "monday": "Monday",
"na": "N/A", "na": "N/A",
"no": "No", "no": "No",
"nointernet": "It looks like you're not connected to the internet.",
"nointernet_sub": "Please check your connection and try again. ",
"none": "None", "none": "None",
"out": "Out", "out": "Out",
"password": "Password", "password": "Password",
@@ -1281,7 +1288,7 @@
"gppercent": "% G.P.", "gppercent": "% G.P.",
"hrs_claimed": "Hours Claimed", "hrs_claimed": "Hours Claimed",
"hrs_total": "Hours Total", "hrs_total": "Hours Total",
"importnote": "The job was initially imported on {{date}} at {{time}}.", "importnote": "The job was initially imported.",
"inproduction": "In Production", "inproduction": "In Production",
"intakechecklist": "Intake Checklist", "intakechecklist": "Intake Checklist",
"job": "Job Details", "job": "Job Details",
@@ -1316,6 +1323,7 @@
"partstotal": "This is the total of all parts and sublet amounts on the vehicle (some of these may require an in-house invoice).<br/>\nItems such as shop and paint materials, labor online lines, etc. are not included in this total.", "partstotal": "This is the total of all parts and sublet amounts on the vehicle (some of these may require an in-house invoice).<br/>\nItems such as shop and paint materials, labor online lines, etc. are not included in this total.",
"totalreturns": "The total amount of returns created for this job." "totalreturns": "The total amount of returns created for this job."
}, },
"prt_dsmk_total": "Line Item Markup",
"rates": "Rates", "rates": "Rates",
"rates_subtotal": "All Rates Subtotal", "rates_subtotal": "All Rates Subtotal",
"reconciliation": { "reconciliation": {
@@ -1341,19 +1349,19 @@
"state_tax_amt": "Provincial/State Taxes", "state_tax_amt": "Provincial/State Taxes",
"subletstotal": "Sublets Total", "subletstotal": "Sublets Total",
"subtotal": "Subtotal", "subtotal": "Subtotal",
"supplementnote": "The job had a supplement imported on {{date}} at {{time}}.", "supplementnote": "The job had a supplement imported.",
"suspense": "Suspense", "suspense": "Suspense",
"total_cost": "Total Cost", "total_cost": "Total Cost",
"total_cust_payable": "Total Customer Amount Payable", "total_cust_payable": "Total Customer Amount Payable",
"total_repairs": "Total Repairs", "total_repairs": "Total Repairs",
"total_sales": "Total Sales", "total_sales": "Total Sales",
"totals": "Totals", "totals": "Totals",
"unvoidnote": "This job was unvoided by {{email}}.", "unvoidnote": "This job was unvoided.",
"vehicle_info": "Vehicle", "vehicle_info": "Vehicle",
"vehicleassociation": "Vehicle Association", "vehicleassociation": "Vehicle Association",
"viewallocations": "View Allocations", "viewallocations": "View Allocations",
"voidjob": "Are you sure you want to void this job? This cannot be easily undone. ", "voidjob": "Are you sure you want to void this job? This cannot be easily undone. ",
"voidnote": "This repair order was voided on {{date}} at {{time}}." "voidnote": "This job was voided."
}, },
"successes": { "successes": {
"addedtoproduction": "Job added to production board.", "addedtoproduction": "Job added to production board.",
@@ -1664,6 +1672,8 @@
"state": "Province/State" "state": "Province/State"
}, },
"labels": { "labels": {
"noneselected": "No phone book entry selected. ",
"onenamerequired": "At least one name related field is required.",
"vendorcategory": "Vendor" "vendorcategory": "Vendor"
}, },
"successes": { "successes": {
@@ -1815,6 +1825,9 @@
}, },
"labels": { "labels": {
"activeshop": "Active Shop" "activeshop": "Active Shop"
},
"successes": {
"updated": "Profile updated successfully."
} }
}, },
"reportcenter": { "reportcenter": {

View File

@@ -218,6 +218,10 @@
"invoice_federal_tax_rate": "", "invoice_federal_tax_rate": "",
"invoice_local_tax_rate": "", "invoice_local_tax_rate": "",
"invoice_state_tax_rate": "", "invoice_state_tax_rate": "",
"jc_hourly_rates": {
"mapa": "",
"mash": ""
},
"lastnumberworkingdays": "", "lastnumberworkingdays": "",
"logo_img_path": "", "logo_img_path": "",
"logo_img_path_height": "", "logo_img_path_height": "",
@@ -683,7 +687,8 @@
"deleting_cloudinary": "", "deleting_cloudinary": "",
"getpresignurl": "Error al obtener la URL prescrita para el documento. {{message}}", "getpresignurl": "Error al obtener la URL prescrita para el documento. {{message}}",
"insert": "Incapaz de cargar el archivo. {{message}}", "insert": "Incapaz de cargar el archivo. {{message}}",
"nodocuments": "No hay documentos" "nodocuments": "No hay documentos",
"updating": ""
}, },
"labels": { "labels": {
"confirmdelete": "", "confirmdelete": "",
@@ -820,6 +825,8 @@
"monday": "", "monday": "",
"na": "N / A", "na": "N / A",
"no": "", "no": "",
"nointernet": "",
"nointernet_sub": "",
"none": "", "none": "",
"out": "Afuera", "out": "Afuera",
"password": "", "password": "",
@@ -1316,6 +1323,7 @@
"partstotal": "", "partstotal": "",
"totalreturns": "" "totalreturns": ""
}, },
"prt_dsmk_total": "",
"rates": "Tarifas", "rates": "Tarifas",
"rates_subtotal": "", "rates_subtotal": "",
"reconciliation": { "reconciliation": {
@@ -1664,6 +1672,8 @@
"state": "" "state": ""
}, },
"labels": { "labels": {
"noneselected": "",
"onenamerequired": "",
"vendorcategory": "" "vendorcategory": ""
}, },
"successes": { "successes": {
@@ -1815,6 +1825,9 @@
}, },
"labels": { "labels": {
"activeshop": "" "activeshop": ""
},
"successes": {
"updated": ""
} }
}, },
"reportcenter": { "reportcenter": {

View File

@@ -218,6 +218,10 @@
"invoice_federal_tax_rate": "", "invoice_federal_tax_rate": "",
"invoice_local_tax_rate": "", "invoice_local_tax_rate": "",
"invoice_state_tax_rate": "", "invoice_state_tax_rate": "",
"jc_hourly_rates": {
"mapa": "",
"mash": ""
},
"lastnumberworkingdays": "", "lastnumberworkingdays": "",
"logo_img_path": "", "logo_img_path": "",
"logo_img_path_height": "", "logo_img_path_height": "",
@@ -683,7 +687,8 @@
"deleting_cloudinary": "", "deleting_cloudinary": "",
"getpresignurl": "Erreur lors de l'obtention de l'URL présignée pour le document. {{message}}", "getpresignurl": "Erreur lors de l'obtention de l'URL présignée pour le document. {{message}}",
"insert": "Incapable de télécharger le fichier. {{message}}", "insert": "Incapable de télécharger le fichier. {{message}}",
"nodocuments": "Il n'y a pas de documents." "nodocuments": "Il n'y a pas de documents.",
"updating": ""
}, },
"labels": { "labels": {
"confirmdelete": "", "confirmdelete": "",
@@ -820,6 +825,8 @@
"monday": "", "monday": "",
"na": "N / A", "na": "N / A",
"no": "", "no": "",
"nointernet": "",
"nointernet_sub": "",
"none": "", "none": "",
"out": "En dehors", "out": "En dehors",
"password": "", "password": "",
@@ -1316,6 +1323,7 @@
"partstotal": "", "partstotal": "",
"totalreturns": "" "totalreturns": ""
}, },
"prt_dsmk_total": "",
"rates": "Les taux", "rates": "Les taux",
"rates_subtotal": "", "rates_subtotal": "",
"reconciliation": { "reconciliation": {
@@ -1664,6 +1672,8 @@
"state": "" "state": ""
}, },
"labels": { "labels": {
"noneselected": "",
"onenamerequired": "",
"vendorcategory": "" "vendorcategory": ""
}, },
"successes": { "successes": {
@@ -1815,6 +1825,9 @@
}, },
"labels": { "labels": {
"activeshop": "" "activeshop": ""
},
"successes": {
"updated": ""
} }
}, },
"reportcenter": { "reportcenter": {

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" ADD CONSTRAINT "bodyshops_autohouseid_key"
UNIQUE ("autohouseid");
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" DROP CONSTRAINT "bodyshops_autohouseid_key";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "jc_hourly_rates";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "jc_hourly_rates" jsonb NULL
DEFAULT jsonb_build_object();
type: run_sql

View File

@@ -0,0 +1,83 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- federal_tax_id
- id
- imexshopid
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- jobsizelimit
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- phone
- prodtargethrs
- production_config
- region_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- stripe_acct_id
- sub_status
- target_touchtime
- template_header
- textid
- updated_at
- use_fippa
- website
- workingdays
- zip_post
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,84 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- federal_tax_id
- id
- imexshopid
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- jc_hourly_rates
- jobsizelimit
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- phone
- prodtargethrs
- production_config
- region_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- stripe_acct_id
- sub_status
- target_touchtime
- template_header
- textid
- updated_at
- use_fippa
- website
- workingdays
- zip_post
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,77 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_update_permission
- args:
permission:
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- federal_tax_id
- id
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- phone
- prodtargethrs
- production_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- target_touchtime
- updated_at
- use_fippa
- website
- workingdays
- zip_post
filter:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
set: {}
role: user
table:
name: bodyshops
schema: public
type: create_update_permission

View File

@@ -0,0 +1,78 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_update_permission
- args:
permission:
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- federal_tax_id
- id
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- jc_hourly_rates
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- phone
- prodtargethrs
- production_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- target_touchtime
- updated_at
- use_fippa
- website
- workingdays
- zip_post
filter:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
set: {}
role: user
table:
name: bodyshops
schema: public
type: create_update_permission

View File

@@ -771,6 +771,7 @@ tables:
- inhousevendorid - inhousevendorid
- insurance_vendor_id - insurance_vendor_id
- intakechecklist - intakechecklist
- jc_hourly_rates
- jobsizelimit - jobsizelimit
- logo_img_path - logo_img_path
- md_categories - md_categories
@@ -841,6 +842,7 @@ tables:
- inhousevendorid - inhousevendorid
- insurance_vendor_id - insurance_vendor_id
- intakechecklist - intakechecklist
- jc_hourly_rates
- logo_img_path - logo_img_path
- md_categories - md_categories
- md_ccc_rates - md_ccc_rates

View File

@@ -203,7 +203,8 @@ const generateInvoiceQbxml = (
//Create the invoice lines mapping. //Create the invoice lines mapping.
jobs_by_pk.joblines.map((jobline) => { jobs_by_pk.joblines.map((jobline) => {
//Parts Lines //Parts Lines
if (jobline.db_ref === "936008") { //If either of these DB REFs change, they also need to change in job-totals calculations. if (jobline.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals calculations.
hasMapaLine = true; hasMapaLine = true;
} }
if (jobline.db_ref === "936007") { if (jobline.db_ref === "936007") {
@@ -213,7 +214,15 @@ const generateInvoiceQbxml = (
if (jobline.profitcenter_part && jobline.act_price) { if (jobline.profitcenter_part && jobline.act_price) {
const DineroAmount = Dinero({ const DineroAmount = Dinero({
amount: Math.round(jobline.act_price * 100), amount: Math.round(jobline.act_price * 100),
}).multiply(jobline.part_qty || 1); })
.multiply(jobline.part_qty || 1)
.add(
Dinero({
amount: Math.round((jobline.act_price || 0) * 100),
})
.multiply(jobline.part_qty || 0)
.percentage(jobline.prt_dsmk_p)
);
const account = responsibilityCenters.profits.find( const account = responsibilityCenters.profits.find(
(i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase()
); );

View File

@@ -566,6 +566,7 @@ exports.GET_JOB_BY_PK = ` query GET_JOB_BY_PK($id: uuid!) {
tax_part tax_part
db_ref db_ref
manual_line manual_line
prt_dsmk_p
parts_order_lines { parts_order_lines {
id id
parts_order { parts_order {
@@ -679,6 +680,7 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
bodyshop{ bodyshop{
id id
md_responsibility_centers md_responsibility_centers
jc_hourly_rates
} }
} }
}`; }`;
@@ -779,6 +781,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
bodyshop { bodyshop {
id id
md_responsibility_centers md_responsibility_centers
jc_hourly_rates
} }
} }
} }

View File

@@ -10,8 +10,7 @@ Dinero.globalRoundingMode = "HALF_EVEN";
async function JobCosting(req, res) { async function JobCosting(req, res) {
const { jobid } = req.body; const { jobid } = req.body;
console.log("🚀 ~ file: job-costing.js ~ line 13 ~ jobid", jobid); console.time("Query for Data");
console.time("querydata");
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
@@ -122,27 +121,34 @@ async function JobCostingMulti(req, res) {
}); });
//Add all summary data. //Add all summary data.
multiSummary.summaryData.totalPartsSales = multiSummary.summaryData.totalPartsSales.add( multiSummary.summaryData.totalPartsSales =
costingData.summaryData.totalPartsSales multiSummary.summaryData.totalPartsSales.add(
); costingData.summaryData.totalPartsSales
multiSummary.summaryData.totalSales = multiSummary.summaryData.totalSales.add( );
costingData.summaryData.totalSales multiSummary.summaryData.totalSales =
); multiSummary.summaryData.totalSales.add(
multiSummary.summaryData.totalLaborCost = multiSummary.summaryData.totalLaborCost.add( costingData.summaryData.totalSales
costingData.summaryData.totalLaborCost );
); multiSummary.summaryData.totalLaborCost =
multiSummary.summaryData.totalLaborSales = multiSummary.summaryData.totalLaborSales.add( multiSummary.summaryData.totalLaborCost.add(
costingData.summaryData.totalLaborSales costingData.summaryData.totalLaborCost
); );
multiSummary.summaryData.totalPartsCost = multiSummary.summaryData.totalPartsCost.add( multiSummary.summaryData.totalLaborSales =
costingData.summaryData.totalPartsCost multiSummary.summaryData.totalLaborSales.add(
); costingData.summaryData.totalLaborSales
multiSummary.summaryData.totalCost = multiSummary.summaryData.totalCost.add( );
costingData.summaryData.totalCost multiSummary.summaryData.totalPartsCost =
); multiSummary.summaryData.totalPartsCost.add(
multiSummary.summaryData.gpdollars = multiSummary.summaryData.gpdollars.add( costingData.summaryData.totalPartsCost
costingData.summaryData.gpdollars );
); multiSummary.summaryData.totalCost =
multiSummary.summaryData.totalCost.add(
costingData.summaryData.totalCost
);
multiSummary.summaryData.gpdollars =
multiSummary.summaryData.gpdollars.add(
costingData.summaryData.gpdollars
);
console.timeEnd(`SummaryOfCostingData-${job.id}`); console.timeEnd(`SummaryOfCostingData-${job.id}`);
//Take the summary data & add it to total summary data. //Take the summary data & add it to total summary data.
}); });
@@ -202,6 +208,8 @@ function GenerateCostingData(job) {
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name) job.bodyshop.md_responsibility_centers.costs.map((p) => p.name)
); );
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
//Massage the data. //Massage the data.
const jobLineTotalsByProfitCenter = const jobLineTotalsByProfitCenter =
job && job &&
@@ -220,14 +228,14 @@ function GenerateCostingData(job) {
}).multiply(val.mod_lb_hrs || 0); }).multiply(val.mod_lb_hrs || 0);
if (!acc.labor[laborProfitCenter]) if (!acc.labor[laborProfitCenter])
acc.labor[laborProfitCenter] = Dinero(); acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add( acc.labor[laborProfitCenter] =
laborAmount acc.labor[laborProfitCenter].add(laborAmount);
);
if (val.mod_lbr_ty === "LAR") { if (val.mod_lbr_ty === "LAR") {
if (!acc.labor[defaultProfits["MAPA"]]) if (!acc.labor[defaultProfits["MAPA"]])
acc.labor[defaultProfits["MAPA"]] = Dinero(); acc.labor[defaultProfits["MAPA"]] = Dinero();
materialsHours.mapaHrs += val.mod_lb_hrs || 0;
acc.labor[defaultProfits["MAPA"]] = acc.labor[ acc.labor[defaultProfits["MAPA"]] = acc.labor[
defaultProfits["MAPA"] defaultProfits["MAPA"]
].add( ].add(
@@ -247,6 +255,7 @@ function GenerateCostingData(job) {
amount: Math.round((job.rate_mash || 0) * 100), amount: Math.round((job.rate_mash || 0) * 100),
}).multiply(val.mod_lb_hrs || 0) }).multiply(val.mod_lb_hrs || 0)
); );
materialsHours.mashHrs += val.mod_lb_hrs || 0;
} }
//If labor line, add to paint and shop materials. //If labor line, add to paint and shop materials.
} }
@@ -265,12 +274,19 @@ function GenerateCostingData(job) {
); );
const partsAmount = Dinero({ const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100), amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1); })
.multiply(val.part_qty || 1)
.add(
Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p)
);
if (!acc.parts[partsProfitCenter]) if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero(); acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add( acc.parts[partsProfitCenter] =
partsAmount acc.parts[partsProfitCenter].add(partsAmount);
);
} }
//To deal with additional costs. //To deal with additional costs.
@@ -287,18 +303,20 @@ function GenerateCostingData(job) {
} else { } else {
const partsAmount = Dinero({ const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100), amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1); })
console.log( .multiply(val.part_qty || 1)
`*** partsAmount`, .add(
val.line_desc, Dinero({
partsProfitCenter, amount: Math.round((val.act_price || 0) * 100),
partsAmount.toJSON() })
); .multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p)
);
if (!acc.parts[partsProfitCenter]) if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero(); acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add( acc.parts[partsProfitCenter] =
partsAmount acc.parts[partsProfitCenter].add(partsAmount);
);
} }
} }
@@ -328,6 +346,52 @@ function GenerateCostingData(job) {
return bill_acc; return bill_acc;
}, {}); }, {});
//If the hourly rates for job costing are set, add them in.
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount:
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0,
}).multiply(materialsHours.mapaHrs)
);
}
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = Dinero();
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
].add(
Dinero({
amount:
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mash * 100) ||
0,
}).multiply(materialsHours.mashHrs)
);
}
const ticketTotalsByCostCenter = job.timetickets.reduce( const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => { (ticket_acc, ticket_val) => {
//At the invoice level. //At the invoice level.

View File

@@ -86,6 +86,7 @@ async function Totals(req, res) {
res.status(400).send(JSON.stringify(error)); res.status(400).send(JSON.stringify(error));
} }
} }
function CalculateRatesTotals(ratesList) { function CalculateRatesTotals(ratesList) {
const jobLines = ratesList.joblines.filter((jl) => !jl.removed); const jobLines = ratesList.joblines.filter((jl) => !jl.removed);
@@ -211,6 +212,13 @@ function CalculatePartsTotals(jobLines) {
...acc, ...acc,
parts: { parts: {
...acc.parts, ...acc.parts,
prt_dsmk_total: acc.parts.prt_dsmk_total.add(
Dinero({
amount: Math.round((value.act_price || 0) * 100),
})
.multiply(value.part_qty || 0)
.percentage(value.prt_dsmk_p)
),
list: { list: {
...acc.parts.list, ...acc.parts.list,
[value.part_type]: [value.part_type]:
@@ -229,11 +237,19 @@ function CalculatePartsTotals(jobLines) {
}).multiply(value.part_qty || 0), }).multiply(value.part_qty || 0),
}, },
}, },
subtotal: acc.parts.subtotal.add( subtotal: acc.parts.subtotal
Dinero({ .add(
amount: Math.round(value.act_price * 100), Dinero({
}).multiply(value.part_qty || 0) amount: Math.round(value.act_price * 100),
), }).multiply(value.part_qty || 0)
)
.add(
Dinero({
amount: Math.round((value.act_price || 0) * 100),
})
.multiply(value.part_qty || 0)
.percentage(value.prt_dsmk_p)
),
}, },
}; };
} }
@@ -241,6 +257,7 @@ function CalculatePartsTotals(jobLines) {
{ {
parts: { parts: {
list: {}, list: {},
prt_dsmk_total: Dinero(),
subtotal: Dinero({ amount: 0 }), subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }), total: Dinero({ amount: 0 }),
}, },
@@ -360,6 +377,13 @@ function CalculateTaxesTotals(job, otherTotals) {
statePartsTax = statePartsTax.add( statePartsTax = statePartsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }) Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1) .multiply(val.part_qty || 1)
.add(
Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p)
)
.percentage( .percentage(
((job.parts_tax_rates && ((job.parts_tax_rates &&
job.parts_tax_rates[val.part_type] && job.parts_tax_rates[val.part_type] &&

View File

@@ -40,7 +40,9 @@ exports.renameKeys = async (req, res) => {
try { try {
const res = { const res = {
id: d.id, id: d.id,
...(await cloudinary.uploader.rename(d.from, d.to)), ...(await cloudinary.uploader.rename(d.from, d.to, {
resource_type: DetermineFileType(d.type),
})),
}; };
return res; return res;
} catch (error) { } catch (error) {
@@ -56,3 +58,14 @@ exports.renameKeys = async (req, res) => {
res.send(result); res.send(result);
}; };
//Also needs to be updated in upload utility and mobile app.
function DetermineFileType(filetype) {
if (!filetype) return "auto";
else if (filetype.startsWith("image")) return "image";
else if (filetype.startsWith("video")) return "video";
else if (filetype.startsWith("application/pdf")) return "image";
else if (filetype.startsWith("application")) return "raw";
return "auto";
}