Merge branch 'release/2023-04-28' into feature/america

This commit is contained in:
Patrick Fic
2023-04-27 15:21:42 -07:00
73 changed files with 3394 additions and 922 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2"> <babeledit_project version="1.2" be_version="2.7.1">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -8974,6 +8974,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>use_paint_scale_data</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>uselocalmediaserver</name> <name>uselocalmediaserver</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -16506,6 +16527,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>excel</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>exceptiontitle</name> <name>exceptiontitle</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33676,6 +33718,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>savetojobnotes</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>
@@ -37798,6 +37861,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dms_posting_sheet</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>envelope_return_address</name> <name>envelope_return_address</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -41451,6 +41535,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>exported_gsr_by_ro</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>exported_gsr_by_ro_labor</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>gsr_by_atp</name> <name>gsr_by_atp</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -143,13 +143,16 @@
} }
} }
//Update row highlighting on production board. //Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td { .ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important; background: #eaeaea !important;
} }
.job-line-manual{ .job-line-manual {
color: tomato; color: tomato;
font-style: italic; font-style: italic;
} }
td.ant-table-column-sort {
background-color: transparent;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,7 +82,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
> >
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} /> <CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
</Form.Item> </Form.Item>
<Space align="end"> <Space align="center">
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt"> <Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">
<CurrencyInput disabled={jobRO} min={0} /> <CurrencyInput disabled={jobRO} min={0} />
</Form.Item> </Form.Item>

View File

@@ -34,8 +34,8 @@ function JobsDocumentsComponent({
const fileType = DetermineFileType(value.type); const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) { if (value.type.startsWith("image")) {
acc.images.push({ acc.images.push({
src: GenerateSrcUrl(value), // src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value), src: GenerateThumbUrl(value),
// src: GenerateSrcUrl(value), // src: GenerateSrcUrl(value),
// thumbnail: GenerateThumbUrl(value), // thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value), fullsize: GenerateSrcUrl(value),

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Gallery } from "react-grid-gallery"; import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility"; import { GenerateThumbUrl } from "./job-documents.utility";
function JobsDocumentGalleryExternal({ function JobsDocumentGalleryExternal({
data, data,
@@ -15,8 +15,8 @@ function JobsDocumentGalleryExternal({
let documents = data.reduce((acc, value) => { let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) { if (value.type.startsWith("image")) {
acc.push({ acc.push({
src: GenerateSrcUrl(value), //src: GenerateSrcUrl(value),
thumbnail: GenerateThumbUrl(value), src: GenerateThumbUrl(value),
thumbnailHeight: 225, thumbnailHeight: 225,
thumbnailWidth: 225, thumbnailWidth: 225,
isSelected: false, isSelected: false,

View File

@@ -52,11 +52,15 @@ function JobDocumentsLocalGalleryExternal({
val.type.mime && val.type.mime &&
val.type.mime.startsWith("image") val.type.mime.startsWith("image")
) { ) {
acc.push(val); acc.push({ ...val, src: val.thumbnail });
} }
return acc; return acc;
}, []) }, [])
: []; : [];
console.log(
"🚀 ~ file: jobs-documents-local-gallery.external.component.jsx:48 ~ useEffect ~ documents:",
documents
);
setgalleryImages(documents); setgalleryImages(documents);
}, [allMedia, jobId, setgalleryImages, t]); }, [allMedia, jobId, setgalleryImages, t]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -241,9 +241,11 @@ export default function ScoreboardTimeTickets() {
); );
ret.totalEffieciencyOverPeriod = ret.totalEffieciencyOverPeriod =
(totalActualAndProductive.totalOverPeriod / totalActualAndProductive.actualTotalOverPeriod
totalActualAndProductive.actualTotalOverPeriod) * ? (totalActualAndProductive.totalOverPeriod /
100; totalActualAndProductive.actualTotalOverPeriod) *
100
: 0;
roundObject(ret); roundObject(ret);
roundObject(totals); roundObject(totals);

View File

@@ -116,7 +116,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
<Col span={12}> <Col span={12}>
<Statistic <Statistic
title={t("scoreboard.labels.efficiencyoverperiod")} title={t("scoreboard.labels.efficiencyoverperiod")}
value={`${data.totalEffieciencyOverPeriod}%`} value={`${data.totalEffieciencyOverPeriod || 0}%`}
/> />
</Col> </Col>
</Row> </Row>

View File

@@ -11,13 +11,13 @@ import {
} from "antd"; } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { import PhoneFormItem, {
PhoneItemFormatterValidation, PhoneItemFormatterValidation,
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import momentTZ from "moment-timezone"; import momentTZ from "moment-timezone";
const timeZonesList = momentTZ.tz.names(); const timeZonesList = momentTZ.tz.names();
@@ -551,6 +551,13 @@ export default function ShopInfoGeneral({ form }) {
> >
<CurrencyInput /> <CurrencyInput />
</Form.Item> </Form.Item>
<Form.Item
name={["use_paint_scale_data"]}
label={t("bodyshop.fields.use_paint_scale_data")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item <Form.Item
name={["attach_pdf_to_email"]} name={["attach_pdf_to_email"]}
label={t("bodyshop.fields.attach_pdf_to_email")} label={t("bodyshop.fields.attach_pdf_to_email")}

View File

@@ -0,0 +1,131 @@
import { useQuery } from "@apollo/client";
import { Card, Col, Space, Statistic, Typography } from "antd";
import moment from "moment";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const { Title } = Typography;
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
});
const mapDispatchToProps = (dispatch) => ({});
const TechJobStatistics = ({ technician }) => {
const { t } = useTranslation();
const startDate = moment().startOf("week");
const endDate = moment().endOf("week");
const { loading, error, data } = useQuery(
QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE,
{
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: moment().startOf("month").format("YYYY-MM-DD"),
fixedEnd: moment().endOf("month").format("YYYY-MM-DD"),
employeeid: technician.id,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);
const totals = useMemo(() => {
if (data && data.timetickets && data.fixedperiod) {
const week = data.timetickets.reduce(
(acc, val) => {
acc.productivehrs = acc.productivehrs + val.productivehrs;
acc.actualhrs = acc.actualhrs + val.actualhrs;
return acc;
},
{ productivehrs: 0, actualhrs: 0 }
);
const month = data.fixedperiod.reduce(
(acc, val) => {
acc.productivehrs = acc.productivehrs + val.productivehrs;
acc.actualhrs = acc.actualhrs + val.actualhrs;
return acc;
},
{ productivehrs: 0, actualhrs: 0 }
);
return {
week,
month,
};
}
return {
week: { productivehrs: 0, actualhrs: 0 },
month: { productivehrs: 0, actualhrs: 0 },
};
}, [data]);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<Card title={t("scoreboard.labels.productivestatistics")}>
<Space size={100}>
<Col>
<Title level={5}>{t("scoreboard.labels.thisweek")}</Title>
<Space size={20}>
<Statistic
title={t("timetickets.fields.productivehrs")}
value={totals.week.productivehrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.fields.actualhrs")}
value={totals.week.actualhrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.labels.efficiency")}
value={
totals.week.actualhrs
? `${(
(totals.week.productivehrs / totals.week.actualhrs) *
100
).toFixed(2)}%`
: "0%"
}
/>
</Space>
</Col>
<Col>
<Title level={5}>{t("scoreboard.labels.thismonth")}</Title>
<Space size={20}>
<Statistic
title={t("timetickets.fields.productivehrs")}
value={totals.month.productivehrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.fields.actualhrs")}
value={totals.month.actualhrs.toFixed(2)}
/>
<Statistic
title={t("timetickets.labels.efficiency")}
value={
totals.month.actualhrs
? `${(
(totals.month.productivehrs / totals.month.actualhrs) *
100
).toFixed(2)}%`
: "0%"
}
/>
</Space>
</Col>
</Space>
</Card>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(TechJobStatistics);

View File

@@ -29,7 +29,17 @@ export function TechSider({ technician, techLogout }) {
}; };
return ( return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}> <Sider
style={{
height: "100vh",
position: "sticky",
top: 0,
left: 0,
}}
collapsible
collapsed={collapsed}
onCollapse={onCollapse}
>
<Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline"> <Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline">
<Menu.Item <Menu.Item
key="1" key="1"

View File

@@ -69,6 +69,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
vendor { vendor {
id id
name name
email
} }
order_date order_date
deliver_by deliver_by
@@ -104,6 +105,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
vendor { vendor {
id id
name name
email
} }
total total
invoice_number invoice_number

View File

@@ -117,6 +117,7 @@ export const QUERY_BODYSHOP = gql`
md_parts_scan md_parts_scan
enforce_conversion_category enforce_conversion_category
tt_enforce_hours_for_tech_console tt_enforce_hours_for_tech_console
use_paint_scale_data
employees { employees {
user_email user_email
id id

View File

@@ -1279,7 +1279,7 @@ export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
export const SEARCH_FOR_JOBS = gql` export const SEARCH_FOR_JOBS = gql`
query SEARCH_FOR_JOBS($search: String!) { query SEARCH_FOR_JOBS($search: String!) {
search_jobs(args: { search: $search }) { search_jobs(args: { search: $search }, limit: 25) {
id id
ro_number ro_number
ownr_fn ownr_fn

View File

@@ -4,7 +4,15 @@ export const INSERT_NEW_NOTE = gql`
mutation INSERT_NEW_NOTE($noteInput: [notes_insert_input!]!) { mutation INSERT_NEW_NOTE($noteInput: [notes_insert_input!]!) {
insert_notes(objects: $noteInput) { insert_notes(objects: $noteInput) {
returning { returning {
created_at
created_by
critical
id id
jobid
private
text
updated_at
audit
} }
} }
} }
@@ -15,8 +23,8 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
jobs_by_pk(id: $id) { jobs_by_pk(id: $id) {
id id
ro_number ro_number
vehicle{ vehicle {
jobs{ jobs {
id id
ro_number ro_number
status status

View File

@@ -19,6 +19,7 @@ export const QUERY_SCOREBOARD = gql`
v_make_desc v_make_desc
v_model_desc v_model_desc
v_model_yr v_model_yr
job_totals
} }
} }
} }

View File

@@ -2,22 +2,20 @@ import { gql } from "@apollo/client";
export const GLOBAL_SEARCH_QUERY = gql` export const GLOBAL_SEARCH_QUERY = gql`
query GLOBAL_SEARCH_QUERY($search: String) { query GLOBAL_SEARCH_QUERY($search: String) {
search_jobs(args: { search: $search }) { search_jobs(args: { search: $search }, limit: 25) {
id id
ro_number ro_number
status status
clm_no clm_no
v_model_yr v_model_yr
v_model_desc v_model_desc
v_make_desc v_make_desc
v_color v_color
ownr_fn ownr_fn
ownr_ln ownr_ln
ownr_co_nm ownr_co_nm
} }
search_owners(args: { search: $search }) { search_owners(args: { search: $search }, limit: 25) {
id id
ownr_fn ownr_fn
ownr_ln ownr_ln
@@ -25,7 +23,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
ownr_ph1 ownr_ph1
ownr_ph2 ownr_ph2
} }
search_vehicles(args: { search: $search }) { search_vehicles(args: { search: $search }, limit: 25) {
id id
v_model_yr v_model_yr
v_model_desc v_model_desc
@@ -34,7 +32,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
v_vin v_vin
plate_no plate_no
} }
search_payments(args: { search: $search }) { search_payments(args: { search: $search }, limit: 25) {
id id
amount amount
paymentnum paymentnum
@@ -45,7 +43,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
memo memo
transactionid transactionid
} }
search_bills(args: { search: $search }) { search_bills(args: { search: $search }, limit: 25) {
id id
date date
invoice_number invoice_number
@@ -54,7 +52,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
name name
} }
} }
search_phonebook(args: { search: $search }) { search_phonebook(args: { search: $search }, limit: 25) {
id id
firstname firstname
lastname lastname

View File

@@ -59,6 +59,81 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
} }
`; `;
export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
query QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE(
$employeeid: uuid!
$start: date!
$end: date!
$fixedStart: date!
$fixedEnd: date!
) {
timetickets(
where: {
date: { _gte: $start, _lte: $end }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
fixedperiod: timetickets(
where: {
date: { _gte: $fixedStart, _lte: $fixedEnd }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
}
`;
export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
query QUERY_TIME_TICKETS_IN_RANGE_SB( query QUERY_TIME_TICKETS_IN_RANGE_SB(
$start: date! $start: date!

View File

@@ -39,7 +39,7 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) {
...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}),
order: [ order: [
{ {
[sortcolumn || "created_at"]: [sortcolumn || "ro_number"]:
sortorder && sortorder !== "false" sortorder && sortorder !== "false"
? sortorder === "descend" ? sortorder === "descend"
? "desc" ? "desc"

View File

@@ -52,6 +52,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -97,7 +98,11 @@ export function JobsDetailPage({
variables: { variables: {
jobId: job.id, jobId: job.id,
job: { job: {
...UndefinedToNull(values, ["alt_transport", "category", "referral_source"]), ...UndefinedToNull(values, [
"alt_transport",
"category",
"referral_source",
]),
parts_tax_rates: { parts_tax_rates: {
...job.parts_tax_rates, ...job.parts_tax_rates,
...values.parts_tax_rates, ...values.parts_tax_rates,
@@ -231,6 +236,7 @@ export function JobsDetailPage({
<ScheduleJobModalContainer /> <ScheduleJobModalContainer />
<JobReconciliationModal /> <JobReconciliationModal />
<JobLineUpsertModalContainer /> <JobLineUpsertModalContainer />
<NoteUpsertModalComponent />
<Form <Form
form={form} form={form}
name="JobDetailForm" name="JobDetailForm"

View File

@@ -2,10 +2,12 @@ import { Divider } from "antd";
import React from "react"; import React from "react";
import TechClockInFormContainer from "../../components/tech-job-clock-in-form/tech-job-clock-in-form.container"; import TechClockInFormContainer from "../../components/tech-job-clock-in-form/tech-job-clock-in-form.container";
import TechClockedInList from "../../components/tech-job-clocked-in-list/tech-job-clocked-in-list.component"; import TechClockedInList from "../../components/tech-job-clocked-in-list/tech-job-clocked-in-list.component";
import TechJobStatistics from "../../components/tech-job-statistics/tech-job-statistics.component";
export default function TechClockComponent() { export default function TechClockComponent() {
return ( return (
<div> <div>
<TechJobStatistics />
<TechClockInFormContainer /> <TechClockInFormContainer />
<Divider /> <Divider />
<TechClockedInList /> <TechClockedInList />

View File

@@ -1,9 +1,10 @@
.tech-content-container { .tech-content-container {
overflow-y: auto; overflow-y: visible;
padding: 1rem; padding: 1rem;
background: #fff; background: #fff;
} }
.tech-layout-container { .tech-layout-container {
height: 100vh; position: relative;
min-height: 100vh;
} }

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs", "tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
"tt_enforce_hours_for_tech_console": "Restrict Claimable hours from Tech Console", "tt_enforce_hours_for_tech_console": "Restrict Claimable hours from Tech Console",
"use_fippa": "Use FIPPA for Names on Generated Documents?", "use_fippa": "Use FIPPA for Names on Generated Documents?",
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
"uselocalmediaserver": "Use Local Media Server?", "uselocalmediaserver": "Use Local Media Server?",
"website": "Website", "website": "Website",
"zip_post": "Zip/Postal Code" "zip_post": "Zip/Postal Code"
@@ -1030,6 +1031,7 @@
"created_at": "Created At", "created_at": "Created At",
"email": "Email", "email": "Email",
"errors": "Errors", "errors": "Errors",
"excel": "Excel",
"exceptiontitle": "An error has occurred.", "exceptiontitle": "An error has occurred.",
"friday": "Friday", "friday": "Friday",
"globalsearch": "Global Search", "globalsearch": "Global Search",
@@ -1977,7 +1979,8 @@
"actions": "Actions", "actions": "Actions",
"deletenote": "Delete Note", "deletenote": "Delete Note",
"edit": "Edit Note", "edit": "Edit Note",
"new": "New Note" "new": "New Note",
"savetojobnotes": "Save to Job Notes"
}, },
"errors": { "errors": {
"inserting": "Error inserting note. {{error}}" "inserting": "Error inserting note. {{error}}"
@@ -2243,6 +2246,7 @@
"csi_invitation": "CSI Invitation", "csi_invitation": "CSI Invitation",
"csi_invitation_action": "CSI Invite", "csi_invitation_action": "CSI Invite",
"diagnostic_authorization": "Diagnostic Authorization", "diagnostic_authorization": "Diagnostic Authorization",
"dms_posting_sheet": "DMS Posting Sheet",
"envelope_return_address": "#10 Envelope Return Address Label", "envelope_return_address": "#10 Envelope Return Address Label",
"estimate": "Estimate Only", "estimate": "Estimate Only",
"estimate_detail": "Estimate Details", "estimate_detail": "Estimate Details",
@@ -2461,6 +2465,8 @@
"export_payables": "Export Log - Payables", "export_payables": "Export Log - Payables",
"export_payments": "Export Log - Payments", "export_payments": "Export Log - Payments",
"export_receivables": "Export Log - Receivables", "export_receivables": "Export Log - Receivables",
"exported_gsr_by_ro": "Exported Gross Sales - Excel",
"exported_gsr_by_ro_labor": "Exported Gross Sales (Labor) - Excel",
"gsr_by_atp": "", "gsr_by_atp": "",
"gsr_by_ats": "Gross Sales by ATS", "gsr_by_ats": "Gross Sales by ATS",
"gsr_by_category": "Gross Sales by Category", "gsr_by_category": "Gross Sales by Category",

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "", "tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "", "tt_enforce_hours_for_tech_console": "",
"use_fippa": "", "use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
"zip_post": "" "zip_post": ""
@@ -1030,6 +1031,7 @@
"created_at": "", "created_at": "",
"email": "", "email": "",
"errors": "", "errors": "",
"excel": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "", "globalsearch": "",
@@ -1977,7 +1979,8 @@
"actions": "Comportamiento", "actions": "Comportamiento",
"deletenote": "Borrar nota", "deletenote": "Borrar nota",
"edit": "Editar nota", "edit": "Editar nota",
"new": "Nueva nota" "new": "Nueva nota",
"savetojobnotes": ""
}, },
"errors": { "errors": {
"inserting": "" "inserting": ""
@@ -2243,6 +2246,7 @@
"csi_invitation": "", "csi_invitation": "",
"csi_invitation_action": "", "csi_invitation_action": "",
"diagnostic_authorization": "", "diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "", "envelope_return_address": "",
"estimate": "", "estimate": "",
"estimate_detail": "", "estimate_detail": "",
@@ -2461,6 +2465,8 @@
"export_payables": "", "export_payables": "",
"export_payments": "", "export_payments": "",
"export_receivables": "", "export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "", "gsr_by_atp": "",
"gsr_by_ats": "", "gsr_by_ats": "",
"gsr_by_category": "", "gsr_by_category": "",

View File

@@ -548,6 +548,7 @@
"tt_allow_post_to_invoiced": "", "tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "", "tt_enforce_hours_for_tech_console": "",
"use_fippa": "", "use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
"zip_post": "" "zip_post": ""
@@ -1030,6 +1031,7 @@
"created_at": "", "created_at": "",
"email": "", "email": "",
"errors": "", "errors": "",
"excel": "",
"exceptiontitle": "", "exceptiontitle": "",
"friday": "", "friday": "",
"globalsearch": "", "globalsearch": "",
@@ -1977,7 +1979,8 @@
"actions": "actes", "actions": "actes",
"deletenote": "Supprimer la note", "deletenote": "Supprimer la note",
"edit": "Note éditée", "edit": "Note éditée",
"new": "Nouvelle note" "new": "Nouvelle note",
"savetojobnotes": ""
}, },
"errors": { "errors": {
"inserting": "" "inserting": ""
@@ -2243,6 +2246,7 @@
"csi_invitation": "", "csi_invitation": "",
"csi_invitation_action": "", "csi_invitation_action": "",
"diagnostic_authorization": "", "diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "", "envelope_return_address": "",
"estimate": "", "estimate": "",
"estimate_detail": "", "estimate_detail": "",
@@ -2461,6 +2465,8 @@
"export_payables": "", "export_payables": "",
"export_payments": "", "export_payments": "",
"export_receivables": "", "export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "", "gsr_by_atp": "",
"gsr_by_ats": "", "gsr_by_ats": "",
"gsr_by_category": "", "gsr_by_category": "",

View File

@@ -20,7 +20,6 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
group: "authorization", group: "authorization",
}, },
fippa_authorization: { fippa_authorization: {
title: i18n.t("printcenter.jobs.fippa_authorization"), title: i18n.t("printcenter.jobs.fippa_authorization"),
description: "CASL Authorization", description: "CASL Authorization",
@@ -101,7 +100,6 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
group: "ro", group: "ro",
}, },
job_notes: { job_notes: {
title: i18n.t("printcenter.jobs.job_notes"), title: i18n.t("printcenter.jobs.job_notes"),
description: "All Jobs Notes", description: "All Jobs Notes",
@@ -330,7 +328,6 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
group: "post", group: "post",
}, },
vehicle_delivery_check: { vehicle_delivery_check: {
title: i18n.t("printcenter.jobs.vehicle_delivery_check"), title: i18n.t("printcenter.jobs.vehicle_delivery_check"),
description: "All Jobs Notes", description: "All Jobs Notes",
@@ -339,7 +336,6 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
group: "post", group: "post",
}, },
guarantee: { guarantee: {
title: i18n.t("printcenter.jobs.guarantee"), title: i18n.t("printcenter.jobs.guarantee"),
description: "All Jobs Notes", description: "All Jobs Notes",
@@ -496,6 +492,14 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
group: "financial", group: "financial",
}, },
dms_posting_sheet: {
title: i18n.t("printcenter.jobs.dms_posting_sheet"),
description: "DMS Posting Sheet",
subject: i18n.t("printcenter.jobs.dms_posting_sheet"),
key: "dms_posting_sheet",
disabled: false,
group: "financial",
},
} }
: {}), : {}),
...(!type || type === "job_special" ...(!type || type === "job_special"
@@ -622,7 +626,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_detail_closed_ins_co: { hours_sold_detail_closed_ins_co: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_ins_co" "reportcenter.templates.hours_sold_detail_closed_ins_co"
@@ -640,7 +643,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_summary_closed: { hours_sold_summary_closed: {
title: i18n.t("reportcenter.templates.hours_sold_summary_closed"), title: i18n.t("reportcenter.templates.hours_sold_summary_closed"),
description: "", description: "",
@@ -654,7 +656,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_summary_closed_ins_co: { hours_sold_summary_closed_ins_co: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_ins_co" "reportcenter.templates.hours_sold_summary_closed_ins_co"
@@ -672,7 +673,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_detail_open: { hours_sold_detail_open: {
title: i18n.t("reportcenter.templates.hours_sold_detail_open"), title: i18n.t("reportcenter.templates.hours_sold_detail_open"),
description: "", description: "",
@@ -686,7 +686,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_detail_open_ins_co: { hours_sold_detail_open_ins_co: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.hours_sold_detail_open_ins_co" "reportcenter.templates.hours_sold_detail_open_ins_co"
@@ -704,7 +703,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_summary_open: { hours_sold_summary_open: {
title: i18n.t("reportcenter.templates.hours_sold_summary_open"), title: i18n.t("reportcenter.templates.hours_sold_summary_open"),
description: "", description: "",
@@ -718,7 +716,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_summary_open_ins_co: { hours_sold_summary_open_ins_co: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.hours_sold_summary_open_ins_co" "reportcenter.templates.hours_sold_summary_open_ins_co"
@@ -736,7 +733,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
hours_sold_detail_closed_csr: { hours_sold_detail_closed_csr: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_csr" "reportcenter.templates.hours_sold_detail_closed_csr"
@@ -1090,7 +1086,6 @@ export const TemplateList = (type, context) => {
}, },
group: "customers", group: "customers",
}, },
schedule: { schedule: {
title: i18n.t("reportcenter.templates.schedule"), title: i18n.t("reportcenter.templates.schedule"),
subject: i18n.t("reportcenter.templates.schedule"), subject: i18n.t("reportcenter.templates.schedule"),
@@ -1102,7 +1097,6 @@ export const TemplateList = (type, context) => {
}, },
group: "customers", group: "customers",
}, },
timetickets: { timetickets: {
title: i18n.t("reportcenter.templates.timetickets"), title: i18n.t("reportcenter.templates.timetickets"),
subject: i18n.t("reportcenter.templates.timetickets"), subject: i18n.t("reportcenter.templates.timetickets"),
@@ -1126,7 +1120,6 @@ export const TemplateList = (type, context) => {
title: i18n.t("reportcenter.templates.attendance_detail"), title: i18n.t("reportcenter.templates.attendance_detail"),
subject: i18n.t("reportcenter.templates.attendance_detail"), subject: i18n.t("reportcenter.templates.attendance_detail"),
key: "attendance_detail", key: "attendance_detail",
disabled: false, disabled: false,
rangeFilter: { rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"), object: i18n.t("reportcenter.labels.objects.timetickets"),
@@ -1138,7 +1131,6 @@ export const TemplateList = (type, context) => {
title: i18n.t("reportcenter.templates.attendance_summary"), title: i18n.t("reportcenter.templates.attendance_summary"),
subject: i18n.t("reportcenter.templates.attendance_summary"), subject: i18n.t("reportcenter.templates.attendance_summary"),
key: "attendance_summary", key: "attendance_summary",
disabled: false, disabled: false,
rangeFilter: { rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"), object: i18n.t("reportcenter.labels.objects.timetickets"),
@@ -1158,7 +1150,6 @@ export const TemplateList = (type, context) => {
}, },
group: "payroll", group: "payroll",
}, },
timetickets_summary: { timetickets_summary: {
title: i18n.t("reportcenter.templates.timetickets_summary"), title: i18n.t("reportcenter.templates.timetickets_summary"),
subject: i18n.t("reportcenter.templates.timetickets_summary"), subject: i18n.t("reportcenter.templates.timetickets_summary"),
@@ -1171,7 +1162,6 @@ export const TemplateList = (type, context) => {
}, },
group: "payroll", group: "payroll",
}, },
estimator_detail: { estimator_detail: {
title: i18n.t("reportcenter.templates.estimator_detail"), title: i18n.t("reportcenter.templates.estimator_detail"),
description: "", description: "",
@@ -1224,7 +1214,6 @@ export const TemplateList = (type, context) => {
}, },
group: "purchases", group: "purchases",
}, },
void_ros: { void_ros: {
title: i18n.t("reportcenter.templates.void_ros"), title: i18n.t("reportcenter.templates.void_ros"),
description: "", description: "",
@@ -1329,7 +1318,6 @@ export const TemplateList = (type, context) => {
}, },
group: "sales", group: "sales",
}, },
gsr_by_estimator: { gsr_by_estimator: {
title: i18n.t("reportcenter.templates.gsr_by_estimator"), title: i18n.t("reportcenter.templates.gsr_by_estimator"),
description: "", description: "",
@@ -1814,7 +1802,9 @@ export const TemplateList = (type, context) => {
group: "jobs", group: "jobs",
}, },
purchase_return_ratio_grouped_by_vendor_detail: { purchase_return_ratio_grouped_by_vendor_detail: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"), title: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"
),
subject: i18n.t( subject: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail" "reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"
), ),
@@ -1828,7 +1818,9 @@ export const TemplateList = (type, context) => {
group: "purchases", group: "purchases",
}, },
purchase_return_ratio_grouped_by_vendor_summary: { purchase_return_ratio_grouped_by_vendor_summary: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"), title: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"
),
subject: i18n.t( subject: i18n.t(
"reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary" "reportcenter.templates.purchase_return_ratio_grouped_by_vendor_summary"
), ),
@@ -1843,9 +1835,7 @@ export const TemplateList = (type, context) => {
}, },
production_over_time: { production_over_time: {
title: i18n.t("reportcenter.templates.production_over_time"), title: i18n.t("reportcenter.templates.production_over_time"),
subject: i18n.t( subject: i18n.t("reportcenter.templates.production_over_time"),
"reportcenter.templates.production_over_time"
),
key: "production_over_time", key: "production_over_time",
//idtype: "vendor", //idtype: "vendor",
disabled: false, disabled: false,
@@ -1857,9 +1847,7 @@ export const TemplateList = (type, context) => {
}, },
customer_list: { customer_list: {
title: i18n.t("reportcenter.templates.customer_list"), title: i18n.t("reportcenter.templates.customer_list"),
subject: i18n.t( subject: i18n.t("reportcenter.templates.customer_list"),
"reportcenter.templates.customer_list"
),
key: "customer_list", key: "customer_list",
//idtype: "vendor", //idtype: "vendor",
disabled: false, disabled: false,
@@ -1869,6 +1857,32 @@ export const TemplateList = (type, context) => {
}, },
group: "customers", group: "customers",
}, },
exported_gsr_by_ro: {
title: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
key: "exported_gsr_by_ro",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported"),
},
group: "sales",
},
exported_gsr_by_ro_labor: {
title: i18n.t("reportcenter.templates.exported_gsr_by_ro_labor"),
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro_labor"),
key: "exported_gsr_by_ro_labor",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported"),
},
group: "sales",
},
} }
: {}), : {}),
...(!type || type === "courtesycarcontract" ...(!type || type === "courtesycarcontract"
@@ -1963,7 +1977,6 @@ export const TemplateList = (type, context) => {
}, },
} }
: {}), : {}),
...(!type || type === "production" ...(!type || type === "production"
? { ? {
production_by_last_name: { production_by_last_name: {

View File

@@ -673,6 +673,33 @@
_eq: true _eq: true
- exported: - exported:
_eq: false _eq: false
event_triggers:
- name: os_bills
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns:
- jobid
- invoice_number
- due_date
- vendorid
- id
- date
retry_conf:
interval_sec: 10
num_retries: 3
timeout_sec: 60
webhook_from_env: HASURA_API_URL
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/opensearch'
version: 2
- table: - table:
name: bodyshops name: bodyshops
schema: public schema: public
@@ -831,6 +858,13 @@
table: table:
name: transitions name: transitions
schema: public schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: tt_approval_queue
schema: public
- name: vehicles - name: vehicles
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -938,6 +972,7 @@
- tt_enforce_hours_for_tech_console - tt_enforce_hours_for_tech_console
- updated_at - updated_at
- use_fippa - use_fippa
- use_paint_scale_data
- uselocalmediaserver - uselocalmediaserver
- website - website
- workingdays - workingdays
@@ -1027,6 +1062,7 @@
- tt_enforce_hours_for_tech_console - tt_enforce_hours_for_tech_console
- updated_at - updated_at
- use_fippa - use_fippa
- use_paint_scale_data
- uselocalmediaserver - uselocalmediaserver
- website - website
- workingdays - workingdays
@@ -2262,6 +2298,13 @@
table: table:
name: timetickets name: timetickets
schema: public schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: employeeid
table:
name: tt_approval_queue
schema: public
insert_permissions: insert_permissions:
- role: user - role: user
permission: permission:
@@ -3145,6 +3188,13 @@
table: table:
name: transitions name: transitions
schema: public schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: jobid
table:
name: tt_approval_queue
schema: public
insert_permissions: insert_permissions:
- role: user - role: user
permission: permission:
@@ -4033,6 +4083,41 @@
template_engine: Kriti template_engine: Kriti
url: '{{$base_url}}/record-handler/arms' url: '{{$base_url}}/record-handler/arms'
version: 2 version: 2
- name: os_jobs
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns:
- v_color
- ownerid
- ownr_fn
- v_model_desc
- ownr_ln
- id
- v_make_desc
- ownr_st
- clm_no
- voided
- status
- ownr_co_nm
- v_model_yr
- v_vin
- converted
retry_conf:
interval_sec: 10
num_retries: 3
timeout_sec: 60
webhook_from_env: HASURA_API_URL
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/opensearch'
version: 2
- table: - table:
name: masterdata name: masterdata
schema: public schema: public
@@ -4457,6 +4542,32 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
event_triggers:
- name: os_owners
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns:
- shopid
- ownr_fn
- id
- ownr_co_nm
- ownr_ln
retry_conf:
interval_sec: 10
num_retries: 3
timeout_sec: 60
webhook_from_env: HASURA_API_URL
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/opensearch'
version: 2
- table: - table:
name: parts_order_lines name: parts_order_lines
schema: public schema: public
@@ -4880,6 +4991,36 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
event_triggers:
- name: os_payments
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns:
- paymentnum
- type
- amount
- date
- transactionid
- memo
- payer
- id
- jobid
retry_conf:
interval_sec: 10
num_retries: 3
timeout_sec: 60
webhook_from_env: HASURA_API_URL
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/opensearch'
version: 2
- table: - table:
name: phonebook name: phonebook
schema: public schema: public
@@ -5210,6 +5351,12 @@
- name: job - name: job
using: using:
foreign_key_constraint_on: jobid foreign_key_constraint_on: jobid
- name: tt_approval_queue
using:
foreign_key_constraint_on: ttapprovalqueueid
- name: user
using:
foreign_key_constraint_on: commited_by
insert_permissions: insert_permissions:
- role: user - role: user
permission: permission:
@@ -5228,6 +5375,8 @@
- ciecacode - ciecacode
- clockoff - clockoff
- clockon - clockon
- commited_by
- committed_at
- cost_center - cost_center
- created_at - created_at
- date - date
@@ -5238,6 +5387,7 @@
- memo - memo
- productivehrs - productivehrs
- rate - rate
- ttapprovalqueueid
- updated_at - updated_at
select_permissions: select_permissions:
- role: user - role: user
@@ -5248,6 +5398,8 @@
- ciecacode - ciecacode
- clockoff - clockoff
- clockon - clockon
- commited_by
- committed_at
- cost_center - cost_center
- created_at - created_at
- date - date
@@ -5258,6 +5410,7 @@
- memo - memo
- productivehrs - productivehrs
- rate - rate
- ttapprovalqueueid
- updated_at - updated_at
filter: filter:
bodyshop: bodyshop:
@@ -5277,6 +5430,8 @@
- ciecacode - ciecacode
- clockoff - clockoff
- clockon - clockon
- commited_by
- committed_at
- cost_center - cost_center
- created_at - created_at
- date - date
@@ -5287,6 +5442,7 @@
- memo - memo
- productivehrs - productivehrs
- rate - rate
- ttapprovalqueueid
- updated_at - updated_at
filter: filter:
bodyshop: bodyshop:
@@ -5397,6 +5553,119 @@
authid: authid:
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
check: {} check: {}
- table:
name: tt_approval_queue
schema: public
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: employee
using:
foreign_key_constraint_on: employeeid
- name: job
using:
foreign_key_constraint_on: jobid
- name: user
using:
foreign_key_constraint_on: approved_by
array_relationships:
- name: timetickets
using:
foreign_key_constraint_on:
column: ttapprovalqueueid
table:
name: timetickets
schema: public
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
columns:
- actualhrs
- approved_at
- approved_by
- bodyshopid
- ciecacode
- cost_center
- created_at
- date
- employeeid
- flat_rate
- id
- jobid
- memo
- productivehrs
- rate
- updated_at
select_permissions:
- role: user
permission:
columns:
- actualhrs
- approved_at
- approved_by
- bodyshopid
- ciecacode
- cost_center
- created_at
- date
- employeeid
- flat_rate
- id
- jobid
- memo
- productivehrs
- rate
- updated_at
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
allow_aggregations: true
update_permissions:
- role: user
permission:
columns:
- actualhrs
- approved_at
- approved_by
- bodyshopid
- ciecacode
- cost_center
- created_at
- date
- employeeid
- flat_rate
- id
- jobid
- memo
- productivehrs
- rate
- updated_at
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: null
- table: - table:
name: users name: users
schema: public schema: public
@@ -5467,6 +5736,20 @@
table: table:
name: parts_orders name: parts_orders
schema: public schema: public
- name: timetickets
using:
foreign_key_constraint_on:
column: commited_by
table:
name: timetickets
schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: approved_by
table:
name: tt_approval_queue
schema: public
insert_permissions: insert_permissions:
- role: user - role: user
permission: permission:
@@ -5652,6 +5935,34 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
event_triggers:
- name: os_vehicles
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns:
- v_model_yr
- plate_no
- id
- v_vin
- v_model_desc
- plate_st
- shopid
retry_conf:
interval_sec: 10
num_retries: 3
timeout_sec: 60
webhook_from_env: HASURA_API_URL
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/opensearch'
version: 2
- table: - table:
name: vendors name: vendors
schema: public schema: public

View File

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

View File

@@ -0,0 +1,2 @@
alter table "public"."timetickets" add column "committed_at" timestamptz
null;

View File

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

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."tt_approval_queue" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "jobid" uuid NOT NULL, "employeeid" uuid NOT NULL, "timeticketid" uuid, "approved_by" text, "approved_at" timestamptz NOT NULL, "actualhrs" numeric NOT NULL DEFAULT 0, "productivehrs" numeric NOT NULL DEFAULT 0, "rate" numeric NOT NULL DEFAULT 0, "flat_rate" boolean NOT NULL DEFAULT true, "ciecacode" text, "cost_center" text NOT NULL, "date" date NOT NULL DEFAULT now(), "memo" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("employeeid") REFERENCES "public"."employees"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("timeticketid") REFERENCES "public"."timetickets"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("approved_by") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_tt_approval_queue_updated_at"
BEFORE UPDATE ON "public"."tt_approval_queue"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_tt_approval_queue_updated_at" ON "public"."tt_approval_queue"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" alter column "approved_at" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" alter column "approved_at" drop not null;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" alter column "memo" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" alter column "memo" drop not null;

View File

@@ -0,0 +1,5 @@
alter table "public"."tt_approval_queue"
add constraint "tt_approval_queue_timeticketid_fkey"
foreign key ("timeticketid")
references "public"."timetickets"
("id") on update cascade on delete cascade;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" drop constraint "tt_approval_queue_timeticketid_fkey";

View File

@@ -0,0 +1,2 @@
alter table "public"."tt_approval_queue" alter column "timeticketid" drop not null;
alter table "public"."tt_approval_queue" add column "timeticketid" uuid;

View File

@@ -0,0 +1 @@
alter table "public"."tt_approval_queue" drop column "timeticketid" cascade;

View File

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

View File

@@ -0,0 +1,2 @@
alter table "public"."timetickets" add column "ttapprovalqueueid" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."timetickets" drop constraint "timetickets_ttapprovalqueueid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."timetickets"
add constraint "timetickets_ttapprovalqueueid_fkey"
foreign key ("ttapprovalqueueid")
references "public"."tt_approval_queue"
("id") on update cascade on delete set null;

View File

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

View File

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

View File

@@ -0,0 +1 @@
alter table "public"."timetickets" drop constraint "timetickets_commited_by_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."timetickets"
add constraint "timetickets_commited_by_fkey"
foreign key ("commited_by")
references "public"."users"
("email") on update restrict on delete restrict;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "use_paint_scale_data" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "use_paint_scale_data" boolean
not null default 'false';

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."jobs_idx_status_hash";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "jobs_idx_status_hash" on
"public"."jobs" using hash ("status");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."idx_jobs_ronumber_btree";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "idx_jobs_ronumber_btree" on
"public"."jobs" using btree ("ro_number");

237
os-loader.js Normal file
View File

@@ -0,0 +1,237 @@
const Dinero = require("dinero.js");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("./server/utils/logger");
const path = require("path");
const client = require("./server/graphql-client/graphql-client").client;
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const { Client, Connection } = require("@opensearch-project/opensearch");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const aws4 = require("aws4");
const { gql } = require("graphql-request");
const gqlclient = require("./server/graphql-client/graphql-client").client;
// const osClient = new Client({
// node: `https://imex:Wl0d8k@!@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com/`,
// });
var host = process.env.OPEN_SEARCH_HOST; // e.g. https://my-domain.region.es.amazonaws.com
const createAwsConnector = (credentials, region) => {
class AmazonConnection extends Connection {
buildRequestObject(params) {
const request = super.buildRequestObject(params);
request.service = "es";
request.region = region;
request.headers = request.headers || {};
request.headers["host"] = request.hostname;
return aws4.sign(request, credentials);
}
}
return {
Connection: AmazonConnection,
};
};
const getClient = async () => {
const credentials = await defaultProvider()();
return new Client({
...createAwsConnector(credentials, "ca-central-1"),
node: host,
});
};
async function OpenSearchUpdateHandler(req, res) {
try {
var osClient = await getClient();
// const osClient = new Client({
// node: `https://imex:password@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com`,
// });
//Clear out all current documents
// const deleteResult = await osClient.deleteByQuery({
// index: ["*"], // ["jobs", "payments", "bills", "vehicles", "owners"],
// body: {
// query: {
// match_all: {},
// },
// },
// });
// return;
var batchSize = 1000;
var promiseQueue = [];
//Jobs Load.
const jobsData = await gqlclient.request(`query{jobs{
id
bodyshopid:shopid
ro_number
clm_no
ownr_fn
ownr_ln
status
ownr_co_nm
v_model_yr
v_make_desc
v_model_desc
}}`);
for (let i = 0; i <= jobsData.jobs.length / batchSize; i++) {
const slicedArray = jobsData.jobs.slice(
i * batchSize,
i * batchSize + batchSize
);
const bulkOperation = [];
slicedArray.forEach((job) => {
bulkOperation.push({ index: { _index: "jobs", _id: job.id } });
bulkOperation.push(job);
});
promiseQueue.push(bulkOperation);
}
//Owner Load
const ownersData = await gqlclient.request(`{
owners {
id
bodyshopid: shopid
ownr_fn
ownr_ln
ownr_co_nm
ownr_ph1
ownr_ph2
}
}
`);
for (let i = 0; i <= ownersData.owners.length / batchSize; i++) {
const slicedArray = ownersData.owners.slice(
i * batchSize,
i * batchSize + batchSize
);
const bulkOperation = [];
slicedArray.forEach((owner) => {
bulkOperation.push({ index: { _index: "owners", _id: owner.id } });
bulkOperation.push(owner);
});
promiseQueue.push(bulkOperation);
}
//Vehicles
const vehiclesData = await gqlclient.request(`{
vehicles {
id
bodyshopid: shopid
v_model_yr
v_model_desc
v_make_desc
v_color
v_vin
plate_no
}
}
`);
for (let i = 0; i <= vehiclesData.vehicles.length / batchSize; i++) {
const slicedArray = vehiclesData.vehicles.slice(
i * batchSize,
i * batchSize + batchSize
);
const bulkOperation = [];
slicedArray.forEach((vehicle) => {
bulkOperation.push({ index: { _index: "vehicles", _id: vehicle.id } });
bulkOperation.push(vehicle);
});
promiseQueue.push(bulkOperation);
}
//payments
const paymentsData = await gqlclient.request(`{
payments {
id
amount
paymentnum
memo
transactionid
job {
id
ro_number
bodyshopid: shopid
}
}
}
`);
for (let i = 0; i <= paymentsData.payments.length / batchSize; i++) {
const slicedArray = paymentsData.payments.slice(
i * batchSize,
i * batchSize + batchSize
);
const bulkOperation = [];
slicedArray.forEach((payment) => {
bulkOperation.push({ index: { _index: "payments", _id: payment.id } });
bulkOperation.push({
..._.omit(payment, ["job"]),
bodyshopid: payment.job.bodyshopid,
});
});
promiseQueue.push(bulkOperation);
}
//bills
const billsData = await gqlclient.request(`{
bills {
id
total
invoice_number
date
vendor {
name
id
}
job {
ro_number
id
bodyshopid: shopid
}
}
}
`);
for (let i = 0; i <= billsData.bills.length / batchSize; i++) {
const slicedArray = billsData.bills.slice(
i * batchSize,
i * batchSize + batchSize
);
const bulkOperation = [];
slicedArray.forEach((bill) => {
bulkOperation.push({ index: { _index: "bills", _id: bill.id } });
bulkOperation.push({
..._.omit(bill, ["job"]),
bodyshopid: bill.job.bodyshopid,
});
});
promiseQueue.push(bulkOperation);
}
//Load the entire queue.
for (const queueItem of promiseQueue) {
const insertJobsBulk = await osClient.bulk({ body: queueItem });
console.log(
` ${insertJobsBulk.body.items.length} Records inserted in ${insertJobsBulk.body.took}.`
);
if (insertJobsBulk.body.errors)
console.error("*** Error while inserting.");
}
} catch (error) {
console.log(error);
}
}
OpenSearchUpdateHandler();

52
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@opensearch-project/opensearch": "^2.2.1",
"aws-sdk": "^2.1326.0", "aws-sdk": "^2.1326.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
@@ -362,6 +363,22 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/@opensearch-project/opensearch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.2.1.tgz",
"integrity": "sha512-8zfQX1acL9eWG+ohIc9nJVT9LSqXCdbVEJs0rCPRtji3XF6ahzsiKmGNTeWLxCPDxWCjAIWq9t95xP3Y5Egi6Q==",
"dependencies": {
"aws4": "^1.11.0",
"debug": "^4.3.1",
"hpagent": "^1.2.0",
"ms": "^2.1.3",
"secure-json-parse": "^2.4.0"
},
"engines": {
"node": ">=10",
"yarn": "^1.22.10"
}
},
"node_modules/@protobufjs/aspromise": { "node_modules/@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3066,6 +3083,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
"integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==",
"engines": {
"node": ">=14"
}
},
"node_modules/href-content": { "node_modules/href-content": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/href-content/-/href-content-2.0.2.tgz", "resolved": "https://registry.npmjs.org/href-content/-/href-content-2.0.2.tgz",
@@ -4972,6 +4997,11 @@
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.3.8", "version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -6580,6 +6610,18 @@
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==" "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
}, },
"@opensearch-project/opensearch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.2.1.tgz",
"integrity": "sha512-8zfQX1acL9eWG+ohIc9nJVT9LSqXCdbVEJs0rCPRtji3XF6ahzsiKmGNTeWLxCPDxWCjAIWq9t95xP3Y5Egi6Q==",
"requires": {
"aws4": "^1.11.0",
"debug": "^4.3.1",
"hpagent": "^1.2.0",
"ms": "^2.1.3",
"secure-json-parse": "^2.4.0"
}
},
"@protobufjs/aspromise": { "@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -8622,6 +8664,11 @@
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g=="
}, },
"hpagent": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
"integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA=="
},
"href-content": { "href-content": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/href-content/-/href-content-2.0.2.tgz", "resolved": "https://registry.npmjs.org/href-content/-/href-content-2.0.2.tgz",
@@ -10042,6 +10089,11 @@
"resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz",
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
}, },
"secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
},
"semver": { "semver": {
"version": "7.3.8", "version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",

View File

@@ -17,7 +17,10 @@
"start": "node server.js" "start": "node server.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/credential-provider-node": "^3.319.0",
"@opensearch-project/opensearch": "^2.2.1",
"aws-sdk": "^2.1326.0", "aws-sdk": "^2.1326.0",
"aws4": "^1.12.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",

View File

@@ -264,6 +264,13 @@ app.post("/newlog", (req, res) => {
logger.log(message, type, user, record, object); logger.log(message, type, user, record, object);
}); });
var os = require("./server/opensearch/os-handler");
app.post(
"/opensearch", //fb.validateFirebaseIdToken,
os.handler
);
app.post("/search", fb.validateFirebaseIdToken, os.search);
var cdkGetMake = require("./server/cdk/cdk-get-makes"); var cdkGetMake = require("./server/cdk/cdk-get-makes");
app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default); app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default);

View File

@@ -767,18 +767,41 @@ const CreateCosts = (job) => {
billTotalsByCostCenters[ billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero(); ] = Dinero();
billTotalsByCostCenters[ if (job.bodyshop.use_paint_scale_data === true) {
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA if (job.mixdata.length > 0) {
] = billTotalsByCostCenters[ billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add( ] = Dinero({
Dinero({ amount: (job.mixdata[0] && job.mixdata[0].totalliquidcost * 100) || 0,
amount: });
(job.bodyshop.jc_hourly_rates && } else {
job.bodyshop.jc_hourly_rates.mapa * 100) || billTotalsByCostCenters[
0, job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
}).multiply(job.job_totals.rates.mapa.hours) ] = 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(job.job_totals.rates.mapa.hours)
);
}
} else {
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 (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
if ( if (

View File

@@ -661,7 +661,6 @@ exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employee
exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
bodyshops_by_pk(id: $bodyshopid){ bodyshops_by_pk(id: $bodyshopid){
id id
shopname shopname
address1 address1
@@ -677,6 +676,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
jc_hourly_rates jc_hourly_rates
cdk_dealerid cdk_dealerid
pbs_serialnumber pbs_serialnumber
use_paint_scale_data
timezone timezone
} }
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
@@ -832,6 +832,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
employee_number employee_number
id id
} }
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
} }
} }
`; `;
@@ -1227,12 +1231,17 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
flat_rate flat_rate
ciecacode ciecacode
} }
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
bodyshop{ bodyshop{
id id
md_responsibility_centers md_responsibility_centers
jc_hourly_rates jc_hourly_rates
cdk_dealerid cdk_dealerid
pbs_serialnumber pbs_serialnumber
use_paint_scale_data
} }
} }
}`; }`;
@@ -1335,12 +1344,17 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
flat_rate flat_rate
ciecacode ciecacode
} }
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
bodyshop { bodyshop {
id id
md_responsibility_centers md_responsibility_centers
jc_hourly_rates jc_hourly_rates
cdk_dealerid cdk_dealerid
pbs_serialnumber pbs_serialnumber
use_paint_scale_data
} }
} }
} }
@@ -1798,3 +1812,11 @@ exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCriti
affected_rows affected_rows
} }
}`; }`;
exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) {
id
shopid
}
}
`;

View File

@@ -621,18 +621,41 @@ function GenerateCostingData(job) {
billTotalsByCostCenters.additionalCosts[ billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero(); ] = Dinero();
billTotalsByCostCenters.additionalCosts[ if (job.bodyshop.use_paint_scale_data === true) {
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA if (job.mixdata.length > 0) {
] = billTotalsByCostCenters.additionalCosts[ billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add( ] = Dinero({
Dinero({ amount: (job.mixdata[0] && job.mixdata[0].totalliquidcost * 100) || 0,
amount: });
(job.bodyshop.jc_hourly_rates && } else {
job.bodyshop.jc_hourly_rates.mapa * 100) || billTotalsByCostCenters.additionalCosts[
0, job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
}).multiply(materialsHours.mapaHrs) ] = billTotalsByCostCenters.additionalCosts[
); 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)
);
}
} else {
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters.additionalCosts[
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 (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {

View File

@@ -20,7 +20,8 @@ async function StatusTransition(req, res) {
res.status(403).send("Unauthorized"); res.status(403).send("Unauthorized");
return; return;
} }
res.sendStatus(200);
return;
const { const {
id: jobid, id: jobid,
status: value, status: value,
@@ -78,7 +79,7 @@ async function StatusTransition(req, res) {
res.sendStatus(200); //.json(ret); res.sendStatus(200); //.json(ret);
} catch (error) { } catch (error) {
logger.log("job-costing-error", "ERROR", req.user.email, jobid, { logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
}); });

View File

@@ -0,0 +1,252 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const path = require("path");
const client = require("../graphql-client/graphql-client").client;
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const { Client, Connection } = require("@opensearch-project/opensearch");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const aws4 = require("aws4");
const { gql } = require("graphql-request");
var host = process.env.OPEN_SEARCH_HOST;
const createAwsConnector = (credentials, region) => {
class AmazonConnection extends Connection {
buildRequestObject(params) {
const request = super.buildRequestObject(params);
request.service = "es";
request.region = region;
request.headers = request.headers || {};
request.headers["host"] = request.hostname;
return aws4.sign(request, credentials);
}
}
return {
Connection: AmazonConnection,
};
};
const getClient = async () => {
const credentials = await defaultProvider()();
return new Client({
...createAwsConnector(credentials, "ca-central-1"),
node: host,
});
};
async function OpenSearchUpdateHandler(req, res) {
try {
var osClient = await getClient();
// const osClient = new Client({
// node: `https://imex:<password>@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com/`,
// });
if (req.body.event.op === "DELETE") {
let response;
response = await osClient.delete({
id: req.body.event.data.old.id,
index: req.body.table.name,
});
res.status(200).json(response.body);
} else {
let document;
switch (req.body.table.name) {
case "jobs":
document = _.pick(req.body.event.data.new, [
"id",
"bodyshopid",
"ro_number",
"clm_no",
"ownr_fn",
"ownr_ln",
"status",
"ownr_co_nm",
"v_model_yr",
"v_make_desc",
"v_model_desc",
]);
document.bodyshopid = req.body.event.data.new.shopid;
break;
case "vehicles":
document = _.pick(req.body.event.data.new, [
"id",
"v_model_yr",
"v_model_desc",
"v_make_desc",
"v_color",
"v_vin",
"plate_no",
]);
document.bodyshopid = req.body.event.data.new.shopid;
break;
case "owners":
document = _.pick(req.body.event.data.new, [
"id",
"ownr_fn",
"ownr_ln",
"ownr_co_nm",
"ownr_ph1",
"ownr_ph2",
]);
document.bodyshopid = req.body.event.data.new.shopid;
break;
case "bills":
const bill = await client.request(
`query ADMIN_GET_BILL_BY_ID($billId: uuid!) {
bills_by_pk(id: $billId) {
id
job {
id
ro_number
shopid
}
vendor {
id
name
}
}
}
`,
{ billId: req.body.event.data.new.id }
);
document = {
..._.pick(req.body.event.data.new, [
"id",
"invoice_number",
"date",
]),
...bill.bills_by_pk,
bodyshopid: bill.bills_by_pk.job.shopid,
};
break;
case "payments":
//Query to get the job and RO number
const payment = await client.request(
`query ADMIN_GET_PAYMENT_BY_ID($paymentId: uuid!) {
payments_by_pk(id: $paymentId) {
id
job {
id
ro_number
shopid
}
}
}
`,
{ paymentId: req.body.event.data.new.id }
);
document = {
..._.pick(req.body.event.data.new, ["id", "invoice_number"]),
...payment.payments_by_pk,
bodyshopid: bill.payments_by_pk.job.shopid,
};
break;
}
const payload = {
id: req.body.event.data.new.id,
index: req.body.table.name,
body: document,
};
let response;
response = await osClient.index(payload);
console.log(response.body);
res.status(200).json(response.body);
}
} catch (error) {
res.status(400).json(JSON.stringify(error));
} finally {
}
}
async function OpensearchSearchHandler(req, res) {
try {
const { search, bodyshopid } = req.body;
if (!req.user) {
res.sendStatus(401);
return;
}
logger.log("os-search", "DEBUG", req.user.email, null, {
search,
});
const BearerToken = req.headers.authorization;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const assocs = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.ACTIVE_SHOP_BY_USER, {
user: req.user.email,
});
if (assocs.length === 0) {
res.sendStatus(401);
}
var osClient = await getClient();
const { body } = await osClient.search({
body: {
size: 100,
query: {
bool: {
must: [
{
multi_match: {
query: search,
type: "phrase_prefix",
//fields: ["*"],
// fuzziness: "5",
//prefix_length: 2,
},
},
{
match: {
bodyshopid: assocs.associations[0].shopid,
},
},
],
},
},
sort: [
{
_score: {
order: "desc",
},
},
],
},
});
res.json(body);
} catch (error) {
console.log(error);
logger.log("os-search-error", "ERROR", req.user.email, null, {
error: JSON.stringify(error),
});
res.status(400).json(error);
} finally {
}
}
exports.handler = OpenSearchUpdateHandler;
exports.search = OpensearchSearchHandler;

2267
yarn.lock

File diff suppressed because it is too large Load Diff