Merged in feature/america (pull request #734)

Feature/america
This commit is contained in:
Patrick Fic
2023-04-27 22:58:39 +00:00
90 changed files with 4515 additions and 1018 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1">
<babeledit_project be_version="2.7.1" version="1.2">
<!--
BabelEdit project file
@@ -6898,6 +6898,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>editcommitted</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>enter</name>
<definition_loaded>false</definition_loaded>
@@ -6963,6 +6984,53 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>ttapprovals</name>
<children>
<concept_node>
<name>approve</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>view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>users</name>
<children>
@@ -7246,6 +7314,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>invoiceexemptcode</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>itemexemptcode</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>la1</name>
<definition_loaded>false</definition_loaded>
@@ -8932,6 +9042,27 @@
</translation>
</translations>
</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>
<name>uselocalmediaserver</name>
<definition_loaded>false</definition_loaded>
@@ -16464,6 +16595,27 @@
</translation>
</translations>
</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>
<name>exceptiontitle</name>
<definition_loaded>false</definition_loaded>
@@ -32471,6 +32623,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttapprovals</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>vehicles</name>
<definition_loaded>false</definition_loaded>
@@ -33634,6 +33807,27 @@
</translation>
</translations>
</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>
</folder_node>
<folder_node>
@@ -37756,6 +37950,27 @@
</translation>
</translations>
</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>
<name>envelope_return_address</name>
<definition_loaded>false</definition_loaded>
@@ -41409,6 +41624,48 @@
</translation>
</translations>
</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>
<name>gsr_by_atp</name>
<definition_loaded>false</definition_loaded>
@@ -44565,6 +44822,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>committed</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>cost_center</name>
<definition_loaded>false</definition_loaded>
@@ -44649,6 +44927,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>employee_team</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>flat_rate</name>
<definition_loaded>false</definition_loaded>
@@ -46270,6 +46569,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttapprovals</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>vehicle-details</name>
<definition_loaded>false</definition_loaded>
@@ -47196,6 +47516,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttapprovals</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>vehicledetail</name>
<definition_loaded>false</definition_loaded>
@@ -47240,6 +47581,37 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>tt_approvals</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>approveselected</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>user</name>
<children>

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 {
background: #eaeaea !important;
}
.job-line-manual{
.job-line-manual {
color: tomato;
font-style: italic;
}
td.ant-table-column-sort {
background-color: transparent;
}

View File

@@ -1,14 +1,15 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button, Form,
Button,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
Table,
Tooltip
Tooltip,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -466,7 +467,7 @@ export function BillEnterModalLinesComponent({
return {
key: `${field.index}fedtax`,
valuePropName: "checked",
initialValue: true,
// initialValue: true,
name: [field.name, "applicable_taxes", "federal"],
};
},

View File

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

View File

@@ -10,7 +10,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
const handleFinish = async (values) => {
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);
};

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 AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
export default function GlobalSearch() {
@@ -18,11 +18,18 @@ export default function GlobalSearch() {
useLazyQuery(GLOBAL_SEARCH_QUERY);
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 handleSearch = (value) => {
console.log("Handle Search");
debouncedExecuteSearch({ variables: { search: value } });
};
@@ -37,7 +44,7 @@ export default function GlobalSearch() {
options: data.search_jobs.map((job) => {
return {
key: job.id,
value: job.ro_number,
value: job.ro_number || "N/A",
label: (
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>

View File

@@ -262,6 +262,11 @@ function Header({
{t("menus.header.timetickets")}
</Link>
</Menu.Item>
<Menu.Item key="ttapprovals" icon={<FieldTimeOutlined />}>
<Link to="/manage/ttapprovals">
{t("menus.header.ttapprovals")}
</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
icon={<Icon component={GiPlayerTime} />}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,7 +92,11 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
to: values.to,
subject: Templates[values.key]?.subject,
},
values.sendby === "email" ? "e" : "p",
values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id
);
setLoading(false);
@@ -250,15 +254,38 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
ranges={DatePIckerRanges}
/>
</Form.Item>
<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 style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
if (reporttype === "excel")
return (
<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>
<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 moment from "moment";
import React from "react";
@@ -18,6 +19,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import _ from "lodash";
import CustomTooltip from "./chart-custom-tooltip";
const graphProps = {
strokeWidth: 3,
@@ -44,14 +46,19 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
return {
bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs,
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 {
dayhrs = {
bodyhrs: 0,
painthrs: 0,
sales: 0,
};
}
@@ -73,6 +80,13 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
: dayhrs.painthrs + dayhrs.bodyhrs,
1
),
sales: _.round(dayhrs.sales, 2),
accSales: _.round(
acc.length > 0
? acc[acc.length - 1].accSales + dayhrs.sales
: dayhrs.sales,
2
),
};
return [...acc, theValue];
@@ -87,22 +101,27 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis strokeWidth={graphProps.strokeWidth} />
<Tooltip />
<Legend />
<Area
type="monotone"
name="Accumulated Hours"
dataKey="accHrs"
fill="lightgreen"
stroke="green"
<YAxis
strokeWidth={graphProps.strokeWidth}
// allowDataOverflow
dataKey="sales"
yAxisId="right"
tickFormatter={(value) =>
Dinero({ amount: Math.round(value * 100) }).toFormat()
}
orientation="right"
/>
<YAxis yAxisId="left" strokeWidth={graphProps.strokeWidth} />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar
name="Body Hours"
dataKey="bodyHrs"
stackId="day"
barSize={20}
fill="darkblue"
yAxisId="left"
/>
<Bar
name="Paint Hours"
@@ -110,12 +129,42 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
stackId="day"
barSize={20}
fill="darkred"
yAxisId="left"
/>
<Line
name="Target Hours"
type="monotone"
dataKey="accTargetHrs"
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}
/>
</ComposedChart>

View File

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

View File

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

View File

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

View File

@@ -532,6 +532,42 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.shop.vendors")}
rules={[

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 (
<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.Item
key="1"

View File

@@ -1,5 +1,5 @@
import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd";
import { Button, Card, Checkbox, Space, Table } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React, { useMemo, useState } from "react";
@@ -62,6 +62,14 @@ export function TimeTicketList({
}, [timetickets]);
const columns = [
{
title: t("timetickets.fields.committed"),
dataIndex: "committed_at",
key: "committed_at",
render: (text, record) => (
<Checkbox disabled checked={record.committed_at} />
),
},
{
title: t("timetickets.fields.date"),
dataIndex: "date",
@@ -236,6 +244,11 @@ export function TimeTicketList({
timeticket: record,
}}
disabled={
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:editcommitted",
}) &&
HasRbacAccess({
bodyshop,
authLevel: authLevel,
@@ -271,7 +284,6 @@ export function TimeTicketList({
}
<Button
onClick={() => {
setTimeTicketTaskContext();
setTimeTicketTaskContext({
actions: {},
context: { jobid: jobId },

View File

@@ -10,6 +10,7 @@ import {
Space,
Table,
} from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -19,10 +20,9 @@ import EmployeeSearchSelectComponent from "../employee-search-select/employee-se
import EmployeeTeamSearchSelectComponent from "../employee-team-search-select/employee-team-search-select.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -39,7 +39,6 @@ export default connect(
export function TimeTicketTaskModalComponent({
bodyshop,
form,
employeeAutoCompleteOptions,
lineTicketCalled,
calculateTimeTickets,
lineTicketLoading,
@@ -72,18 +71,6 @@ export function TimeTicketTaskModalComponent({
/>
</Form.Item>
<Form.Item name="employeeid" label={t("timetickets.fields.employee")}>
<EmployeeSearchSelectComponent
options={employeeAutoCompleteOptions}
allowClear
onSelect={(value) => {
const emps =
employeeAutoCompleteOptions &&
employeeAutoCompleteOptions.filter((e) => e.id === value)[0];
form.setFieldsValue({ flat_rate: emps && emps.flat_rate });
}}
/>
</Form.Item>
<Form.Item
name="employeeteamid"
label={t("timetickets.fields.employee_team")}
@@ -243,7 +230,13 @@ export function TimeTicketTaskModalComponent({
{errors.map((e, idx) => (
<Alert key={idx} message={e} />
))}
<div style={{ display: "none" }}>
<div
style={
{
//display: "none"
}
}
>
{fields.map((field, index) => (
<Form.Item
key={field.key}
@@ -358,7 +351,7 @@ export function TimeTicketTaskModalComponent({
!lineTicketLoading)
) {
queryJobInfo({ variables: { id: jobid } }).then(() =>
calculateTimeTickets()
calculateTimeTickets("")
);
}
return (

View File

@@ -15,6 +15,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicketTasks } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketTaskModalComponent from "./time-ticket-task-modal.component";
import { INSERT_NEW_TT_APPROVALS } from "../../graphql/tt-approvals.queries";
const mapStateToProps = createStructuredSelector({
timeTicketTasksModal: selectTimeTicketTasks,
@@ -40,8 +41,10 @@ export function TimeTickeTaskModalContainer({
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const [insertTimeTickets] = useMutation(INSERT_NEW_TIME_TICKET);
const [insertTimeTicketApproval] = useMutation(INSERT_NEW_TT_APPROVALS);
const [queryJobInfo, { called, loading, data: lineTicketData }] =
useLazyQuery(GET_JOB_INFO_DRAW_CALCULATIONS, {
fetchPolicy: "network-only",
@@ -49,31 +52,53 @@ export function TimeTickeTaskModalContainer({
});
async function handleFinish(values) {
console.log(
"🚀 ~ file: time-ticket-task-modal.container.jsx:52 ~ handleFinish ~ values:",
values
);
try {
const result = await insertTimeTickets({
variables: {
timeTicketInput: values.timetickets.map((ticket) =>
_.omit(ticket, "pay")
),
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
if (true) {
const result = await insertTimeTicketApproval({
variables: {
timeTicketInput: values.timetickets.map((ticket) => ({
..._.omit(ticket, "pay"),
bodyshopid: bodyshop.id,
})),
},
});
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.created"),
});
form.resetFields();
toggleModalVisible();
}
} else {
notification.open({
type: "success",
message: t("timetickets.successes.created"),
const result = await insertTimeTickets({
variables: {
timeTicketInput: values.timetickets.map((ticket) =>
_.omit(ticket, "pay")
),
},
});
toggleModalVisible();
if (result.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.created"),
});
toggleModalVisible();
}
}
} catch (error) {
} finally {
@@ -81,13 +106,13 @@ export function TimeTickeTaskModalContainer({
}
useEffect(() => {
if (context.jobid) {
console.log("UE Fired.");
if (visible && context.jobid) {
queryJobInfo({ variables: { id: context.jobid } });
}
}, [context.jobid, queryJobInfo]);
}, [context.jobid, queryJobInfo, visible]);
const calculateTimeTickets = (presetMemo) => {
console.log("🚀 ~ file: time-ticket-task-modal.container.jsx:115 ~ calculateTimeTickets ~ presetMemo:", presetMemo)
const formData = form.getFieldsValue();
if (
!formData.jobid ||
@@ -99,14 +124,13 @@ export function TimeTickeTaskModalContainer({
) {
return;
}
let data = [];
let eligibleHours = 0;
const theTeam = JSON.parse(formData.employeeteamid);
if (theTeam) {
data = [];
formData.hourstype.forEach((hourstype) => {
eligibleHours =
lineTicketData.joblines.reduce(
@@ -125,6 +149,7 @@ export function TimeTickeTaskModalContainer({
actualhrs: 0,
memo: presetMemo,
flat_rate: true,
ciecacode: hourstype,
cost_center:
bodyshop.md_responsibility_centers.defaults.costs[hourstype],
productivehrs:
@@ -152,7 +177,10 @@ export function TimeTickeTaskModalContainer({
<Modal
destroyOnClose
open={visible}
onCancel={() => toggleModalVisible()}
onCancel={() => {
toggleModalVisible();
form.resetFields();
}}
width="80%"
onOk={() => form.submit()}
>
@@ -161,7 +189,6 @@ export function TimeTickeTaskModalContainer({
form={form}
layout="vertical"
onFinish={handleFinish}
// onFieldsChange={handleFieldsChange}
initialValues={context}
>
<TimeTicketTaskModalComponent

View File

@@ -15,6 +15,7 @@ export default function TimeTicketsTasksPresets({
percent: preset.percent,
hourstype: preset.hourstype,
});
calculateTimeTickets(preset.memo);
}
};

View File

@@ -0,0 +1,241 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Space, Table, Tag } from "antd";
import Dinero from "dinero.js";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../utils/sorters";
import TtApproveButtonComponent from "../tt-approve-button/tt-approve-button.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
});
const mapDispatchToProps = (dispatch) => ({
setTimeTicketTaskContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TtApprovalsListComponent);
export function TtApprovalsListComponent({
bodyshop,
setTimeTicketTaskContext,
authLevel,
disabled,
loading,
tt_approval_queue,
total,
refetch,
techConsole,
jobId,
extra,
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const { t } = useTranslation();
const history = useHistory();
const search = queryString.parse(useLocation().search);
const { page } = search;
const [selectedTickets, setSelectedTickets] = useState([]);
const columns = [
{
title: t("timetickets.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("timetickets.fields.employee"),
dataIndex: "employeeid",
key: "employeeid",
sorter: (a, b) => alphaSort(a.employee.last_name, b.employee.last_name),
sortOrder:
state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
render: (text, record) =>
`${record.employee.first_name} ${record.employee.last_name}`,
filters:
tt_approval_queue
.map((l) => l.employeeid)
.filter(onlyUnique)
.map((s) => {
return {
text: (() => {
const emp = bodyshop.employees.find((e) => e.id === s);
return `${emp?.first_name} ${emp?.last_name}`;
})(), //
value: [s],
};
}) || [],
onFilter: (value, record) => value.includes(record.employeeid),
},
{
title: t("timetickets.fields.cost_center"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
render: (text, record) =>
record.cost_center === "timetickets.labels.shift"
? t(record.cost_center)
: record.cost_center,
sortOrder:
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
filters:
tt_approval_queue
.map((l) => l.cost_center)
.filter(onlyUnique)
.map((s) => {
return {
text: s === "timetickets.labels.shift" ? t(s) : s, //|| "No Status*",
value: [s],
};
}) || [],
onFilter: (value, record) => value.includes(record.cost_center),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) =>
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) =>
record.job && (
<Link to={"/manage/jobs/" + record.job.id}>
<Space wrap>
{record.job.ro_number || "N/A"}
<Tag>{record.job.status}</Tag>
</Space>
</Link>
),
},
{
title: t("timetickets.fields.productivehrs"),
dataIndex: "productivehrs",
key: "productivehrs",
sorter: (a, b) => a.productivehrs - b.productivehrs,
sortOrder:
state.sortedInfo.columnKey === "productivehrs" &&
state.sortedInfo.order,
},
{
title: t("timetickets.fields.actualhrs"),
dataIndex: "actualhrs",
key: "actualhrs",
sorter: (a, b) => a.actualhrs - b.actualhrs,
sortOrder:
state.sortedInfo.columnKey === "actualhrs" && state.sortedInfo.order,
},
{
title: t("timetickets.fields.memo"),
dataIndex: "memo",
key: "memo",
sorter: (a, b) => alphaSort(a.memo, b.memo),
sortOrder:
state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
render: (text, record) =>
record.clockon || record.clockoff ? t(record.memo) : record.memo,
},
{
title: t("timetickets.fields.clockon"),
dataIndex: "clockon",
key: "clockon",
render: (text, record) => (
<DateTimeFormatter>{record.clockon}</DateTimeFormatter>
),
},
{
title: "Pay",
dataIndex: "pay",
key: "pay",
render: (text, record) =>
Dinero({ amount: Math.round(record.rate * 100) })
.multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
.toFormat("$0.00"),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
search.page = pagination.current;
if (sorter && sorter.column && sorter.column.sortObject) {
search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
} else {
delete search.searchObj;
search.sortcolumn = sorter.order ? sorter.columnKey : null;
search.sortorder = sorter.order;
}
search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order });
history.push({ search: queryString.stringify(search) });
};
return (
<Card
title={t("timetickets.labels.timetickets")}
extra={
<Space wrap>
{extra}
<TtApproveButtonComponent
selectedTickets={selectedTickets}
disabled={selectedTickets.length === 0}
completedCallback={setSelectedTickets}
refetch={refetch}
/>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
</Space>
}
>
<Table
loading={loading}
columns={columns}
rowKey="id"
scroll={{
x: true,
}}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}
dataSource={tt_approval_queue}
onChange={handleTableChange}
rowSelection={{
onSelectAll: (selected, selectedRows) =>
setSelectedTickets(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedTickets(selectedRows.map((i) => i.id));
},
selectedRowKeys: selectedTickets,
type: "checkbox",
}}
/>
</Card>
);
}

View File

@@ -0,0 +1,69 @@
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import React from "react";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_TT_APPROVALS_PAGINATED } from "../../graphql/tt-approvals.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import AlertComponent from "../alert/alert.component";
import TtApprovalsListComponent from "./tt-approvals-list.component";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function TimeTicketsContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
}) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, searchObj } = searchParams;
const { loading, error, data, refetch } = useQuery(
QUERY_ALL_TT_APPROVALS_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0,
limit: 25,
order: [
searchObj
? JSON.parse(searchObj)
: {
[sortcolumn || "date"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
}
);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<TtApprovalsListComponent
loading={loading}
tt_approval_queue={data ? data.tt_approval_queue : []}
total={data ? data.tt_approval_queue_aggregate.aggregate.count : 0}
refetch={refetch}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TimeTicketsContainer);

View File

@@ -0,0 +1,92 @@
import { useApolloClient } from "@apollo/client";
import { Button, notification } from "antd";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_TIME_TICKET_AND_APPROVE } from "../../graphql/timetickets.queries";
import { QUERY_TT_APPROVALS_BY_IDS } from "../../graphql/tt-approvals.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
export function TtApproveButton({
bodyshop,
currentUser,
selectedTickets,
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
setLoading(true);
try {
const { data } = await client.query({
query: QUERY_TT_APPROVALS_BY_IDS,
variables: { ids: selectedTickets },
});
const insertResponse = await client.mutate({
mutation: INSERT_TIME_TICKET_AND_APPROVE,
variables: {
timeTicketInput: data.tt_approval_queue.map((tta) => ({
..._.omit(tta, ["id", "__typename"]),
ttapprovalqueueid: tta.id,
})),
approvalIds: selectedTickets,
approvalUpdate: {
approved_at: moment(),
approved_by: currentUser.email,
},
},
});
if (insertResponse.errors) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: JSON.stringify(insertResponse.errors),
}),
});
} else {
notification.open({
type: "success",
message: t("timetickets.successes.createdg"),
});
}
} catch (error) {
notification.open({
type: "error",
message: t("timetickets.errors.creating", {
message: error.message,
}),
});
} finally {
setLoading(false);
}
// if (!!completedCallback) completedCallback([]);
// if (!!loadingCallback) loadingCallback(false);
};
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("tt_approvals.actions.approveselected")}
</Button>
);
}
export default connect(mapStateToProps, null)(TtApproveButton);

View File

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

View File

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

View File

@@ -51,6 +51,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
op_code_desc
convertedtolbr
convertedtolbr_data
}
timetickets(where: { jobid: { _eq: $id } }) {
actualhrs
@@ -65,6 +66,8 @@ export const GET_LINE_TICKET_BY_PK = gql`
clockon
clockoff
rate
committed_at
committed_by
employee {
id
first_name

View File

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

View File

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

View File

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

View File

@@ -2,22 +2,20 @@ import { gql } from "@apollo/client";
export const GLOBAL_SEARCH_QUERY = gql`
query GLOBAL_SEARCH_QUERY($search: String) {
search_jobs(args: { search: $search }) {
search_jobs(args: { search: $search }, limit: 25) {
id
ro_number
status
clm_no
v_model_yr
v_model_desc
v_make_desc
v_color
ownr_fn
ownr_ln
ownr_co_nm
}
search_owners(args: { search: $search }) {
search_owners(args: { search: $search }, limit: 25) {
id
ownr_fn
ownr_ln
@@ -25,7 +23,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
ownr_ph1
ownr_ph2
}
search_vehicles(args: { search: $search }) {
search_vehicles(args: { search: $search }, limit: 25) {
id
v_model_yr
v_model_desc
@@ -34,7 +32,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
v_vin
plate_no
}
search_payments(args: { search: $search }) {
search_payments(args: { search: $search }, limit: 25) {
id
amount
paymentnum
@@ -45,7 +43,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
memo
transactionid
}
search_bills(args: { search: $search }) {
search_bills(args: { search: $search }, limit: 25) {
id
date
invoice_number
@@ -54,7 +52,7 @@ export const GLOBAL_SEARCH_QUERY = gql`
name
}
}
search_phonebook(args: { search: $search }) {
search_phonebook(args: { search: $search }, limit: 25) {
id
firstname
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`
query QUERY_TIME_TICKETS_IN_RANGE_SB(
$start: date!
@@ -83,6 +158,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
productivehrs
memo
jobid
committed_at
committed_by
flat_rate
job {
id
@@ -149,6 +226,39 @@ export const INSERT_NEW_TIME_TICKET = gql`
}
`;
export const INSERT_TIME_TICKET_AND_APPROVE = gql`
mutation INSERT_TIME_TICKET_AND_APPROVE(
$timeTicketInput: [timetickets_insert_input!]!
$approvalIds: [uuid!]!
$approvalUpdate: tt_approval_queue_set_input
) {
insert_timetickets(objects: $timeTicketInput) {
returning {
id
clockon
clockoff
employeeid
productivehrs
actualhrs
ciecacode
date
memo
flat_rate
}
}
update_tt_approval_queue(
where: { id: { _in: $approvalIds } }
_set: $approvalUpdate
) {
returning {
id
approved_at
approved_at
}
}
}
`;
export const UPDATE_TIME_TICKET = gql`
mutation UPDATE_TIME_TICKET(
$timeticketId: uuid!

View File

@@ -0,0 +1,98 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_TT_APPROVALS_PAGINATED = gql`
query QUERY_ALL_TT_APPROVALS_PAGINATED(
$offset: Int
$limit: Int
$order: [tt_approval_queue_order_by!]!
) {
tt_approval_queue(
offset: $offset
limit: $limit
order_by: $order
where: { approved_at: { _is_null: true } }
) {
id
jobid
bodyshopid
employeeid
employee {
first_name
last_name
id
}
job {
ro_number
status
id
}
employeeid
actualhrs
productivehrs
ciecacode
cost_center
date
rate
}
tt_approval_queue_aggregate {
aggregate {
count(distinct: true)
}
}
}
`;
export const INSERT_NEW_TT_APPROVALS = gql`
mutation INSERT_NEW_TT_APPROVALS(
$timeTicketInput: [tt_approval_queue_insert_input!]!
) {
insert_tt_approval_queue(objects: $timeTicketInput) {
returning {
id
employeeid
productivehrs
actualhrs
ciecacode
date
memo
flat_rate
}
}
}
`;
export const QUERY_TT_APPROVALS_BY_IDS = gql`
query QUERY_TT_APPROVALS_BY_IDS($ids: [uuid!]!) {
tt_approval_queue(where: { id: { _in: $ids } }) {
id
productivehrs
actualhrs
rate
memo
jobid
flat_rate
employeeid
date
ciecacode
bodyshopid
cost_center
}
}
`;
export const UPDATE_TT_BY_APPROVAL = gql`
mutation UPDATE_TT_BY_APPROVAL(
$ttApprovalUpdates: [tt_approval_queue_updates!]!
) {
update_tt_approval_queue_many(updates: $ttApprovalUpdates) {
returning {
id
approved_at
approved_by
timeticket {
id
}
}
}
}
`;

View File

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

View File

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

View File

@@ -174,6 +174,9 @@ const Dms = lazy(() => import("../dms/dms.container"));
const DmsPayables = lazy(() =>
import("../dms-payables/dms-payables.container")
);
const TtApprovals = lazy(() =>
import("../tt-approvals/tt-approvals.page.container")
);
const { Content, Footer } = Layout;
@@ -376,6 +379,7 @@ export function Manage({ match, conflict, bodyshop }) {
path={`${match.path}/accounting/exportlogs`}
component={ExportLogs}
/>
<Route exact path={`${match.path}/ttapprovals`} component={TtApprovals} />
<Route exact path={`${match.path}/partsqueue`} component={PartsQueue} />
<Route exact path={`${match.path}/phonebook`} component={Phonebook} />

View File

@@ -2,10 +2,12 @@ import { Divider } from "antd";
import React from "react";
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 TechJobStatistics from "../../components/tech-job-statistics/tech-job-statistics.component";
export default function TechClockComponent() {
return (
<div>
<TechJobStatistics />
<TechClockInFormContainer />
<Divider />
<TechClockedInList />

View File

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

View File

@@ -0,0 +1,42 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import TtApprovalsList from "../../components/tt-approvals-list/tt-approvals-list.container";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function TtApprovalsPage({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.ttapprovals");
setSelectedHeader("ttapprovals");
setBreadcrumbs([
{
link: "/manage/ttapprovals",
label: t("titles.bc.ttapprovals"),
},
]);
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<RbacWrapper action="ttapprovals:view">
<TtApprovalsList />
</RbacWrapper>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TtApprovalsPage);

View File

@@ -435,10 +435,15 @@
},
"timetickets": {
"edit": "Time Tickets -> Edit",
"editcommitted": "Time Tickets -> Edit Committed",
"enter": "Time Tickets -> Enter",
"list": "Time Tickets -> List",
"shiftedit": "Time Tickets -> Shift Edit"
},
"ttapprovals": {
"approve": "Time Ticket Approval -> Approve",
"view": "Time Ticket Approval -> View"
},
"users": {
"editaccess": "Users -> Edit access"
}
@@ -456,6 +461,8 @@
"federal_tax": "Federal Tax",
"federal_tax_itc": "Federal Tax Credit",
"gst_override": "GST Override Account #",
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
"la1": "LA1",
"la2": "LA2",
"la3": "LA3",
@@ -546,6 +553,7 @@
"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",
"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?",
"website": "Website",
"zip_post": "Zip/Postal Code"
@@ -1028,6 +1036,7 @@
"created_at": "Created At",
"email": "Email",
"errors": "Errors",
"excel": "Excel",
"exceptiontitle": "An error has occurred.",
"friday": "Friday",
"globalsearch": "Global Search",
@@ -1903,6 +1912,7 @@
"shop_vendors": "Vendors",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
"ttapprovals": "Time Ticket Approvals",
"vehicles": "Vehicles"
},
"jobsactions": {
@@ -1975,7 +1985,8 @@
"actions": "Actions",
"deletenote": "Delete Note",
"edit": "Edit Note",
"new": "New Note"
"new": "New Note",
"savetojobnotes": "Save to Job Notes"
},
"errors": {
"inserting": "Error inserting note. {{error}}"
@@ -2241,6 +2252,7 @@
"csi_invitation": "CSI Invitation",
"csi_invitation_action": "CSI Invite",
"diagnostic_authorization": "Diagnostic Authorization",
"dms_posting_sheet": "DMS Posting Sheet",
"envelope_return_address": "#10 Envelope Return Address Label",
"estimate": "Estimate Only",
"estimate_detail": "Estimate Details",
@@ -2459,6 +2471,8 @@
"export_payables": "Export Log - Payables",
"export_payments": "Export Log - Payments",
"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_ats": "Gross Sales by ATS",
"gsr_by_category": "Gross Sales by Category",
@@ -2641,10 +2655,12 @@
"clockhours": "Clock Hours",
"clockoff": "Clock Off",
"clockon": "Clocked In",
"committed": "Committed",
"cost_center": "Cost Center",
"date": "Ticket Date",
"efficiency": "Efficiency",
"employee": "Employee",
"employee_team": "Employee Team",
"flat_rate": "Flat Rate?",
"memo": "Memo",
"productivehrs": "Productive Hours",
@@ -2731,6 +2747,7 @@
"shop-vendors": "Vendors",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
"ttapprovals": "Time Ticket Approvals",
"vehicle-details": "Vehicle: {{vehicle}}",
"vehicles": "Vehicles"
},
@@ -2776,9 +2793,15 @@
"shop_vendors": "Vendors | $t(titles.app)",
"temporarydocs": "Temporary Documents | $t(titles.app)",
"timetickets": "Time Tickets | $t(titles.app)",
"ttapprovals": "Time Ticket Approvals | $t(titles.app)",
"vehicledetail": "Vehicle Details {{vehicle}} | $t(titles.app)",
"vehicles": "All Vehicles | $t(titles.app)"
},
"tt_approvals": {
"actions": {
"approveselected": "Approve Selected"
}
},
"user": {
"actions": {
"changepassword": "Change Password",

View File

@@ -435,10 +435,15 @@
},
"timetickets": {
"edit": "",
"editcommitted": "",
"enter": "",
"list": "",
"shiftedit": ""
},
"ttapprovals": {
"approve": "",
"view": ""
},
"users": {
"editaccess": ""
}
@@ -456,6 +461,8 @@
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"invoiceexemptcode": "",
"itemexemptcode": "",
"la1": "",
"la2": "",
"la3": "",
@@ -546,6 +553,7 @@
"tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
@@ -1028,6 +1036,7 @@
"created_at": "",
"email": "",
"errors": "",
"excel": "",
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
@@ -1903,6 +1912,7 @@
"shop_vendors": "Vendedores",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicles": "Vehículos"
},
"jobsactions": {
@@ -1975,7 +1985,8 @@
"actions": "Comportamiento",
"deletenote": "Borrar nota",
"edit": "Editar nota",
"new": "Nueva nota"
"new": "Nueva nota",
"savetojobnotes": ""
},
"errors": {
"inserting": ""
@@ -2241,6 +2252,7 @@
"csi_invitation": "",
"csi_invitation_action": "",
"diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "",
"estimate": "",
"estimate_detail": "",
@@ -2459,6 +2471,8 @@
"export_payables": "",
"export_payments": "",
"export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "",
"gsr_by_ats": "",
"gsr_by_category": "",
@@ -2641,10 +2655,12 @@
"clockhours": "",
"clockoff": "",
"clockon": "",
"committed": "",
"cost_center": "",
"date": "",
"efficiency": "",
"employee": "",
"employee_team": "",
"flat_rate": "",
"memo": "",
"productivehrs": "",
@@ -2731,6 +2747,7 @@
"shop-vendors": "",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicle-details": "",
"vehicles": ""
},
@@ -2776,9 +2793,15 @@
"shop_vendors": "Vendedores | $t(titles.app)",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicledetail": "Detalles del vehículo {{vehicle}} | $t(titles.app)",
"vehicles": "Todos los vehiculos | $t(titles.app)"
},
"tt_approvals": {
"actions": {
"approveselected": ""
}
},
"user": {
"actions": {
"changepassword": "",

View File

@@ -435,10 +435,15 @@
},
"timetickets": {
"edit": "",
"editcommitted": "",
"enter": "",
"list": "",
"shiftedit": ""
},
"ttapprovals": {
"approve": "",
"view": ""
},
"users": {
"editaccess": ""
}
@@ -456,6 +461,8 @@
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"invoiceexemptcode": "",
"itemexemptcode": "",
"la1": "",
"la2": "",
"la3": "",
@@ -546,6 +553,7 @@
"tt_allow_post_to_invoiced": "",
"tt_enforce_hours_for_tech_console": "",
"use_fippa": "",
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
@@ -1028,6 +1036,7 @@
"created_at": "",
"email": "",
"errors": "",
"excel": "",
"exceptiontitle": "",
"friday": "",
"globalsearch": "",
@@ -1903,6 +1912,7 @@
"shop_vendors": "Vendeurs",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicles": "Véhicules"
},
"jobsactions": {
@@ -1975,7 +1985,8 @@
"actions": "actes",
"deletenote": "Supprimer la note",
"edit": "Note éditée",
"new": "Nouvelle note"
"new": "Nouvelle note",
"savetojobnotes": ""
},
"errors": {
"inserting": ""
@@ -2241,6 +2252,7 @@
"csi_invitation": "",
"csi_invitation_action": "",
"diagnostic_authorization": "",
"dms_posting_sheet": "",
"envelope_return_address": "",
"estimate": "",
"estimate_detail": "",
@@ -2459,6 +2471,8 @@
"export_payables": "",
"export_payments": "",
"export_receivables": "",
"exported_gsr_by_ro": "",
"exported_gsr_by_ro_labor": "",
"gsr_by_atp": "",
"gsr_by_ats": "",
"gsr_by_category": "",
@@ -2641,10 +2655,12 @@
"clockhours": "",
"clockoff": "",
"clockon": "",
"committed": "",
"cost_center": "",
"date": "",
"efficiency": "",
"employee": "",
"employee_team": "",
"flat_rate": "",
"memo": "",
"productivehrs": "",
@@ -2731,6 +2747,7 @@
"shop-vendors": "",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicle-details": "",
"vehicles": ""
},
@@ -2776,9 +2793,15 @@
"shop_vendors": "Vendeurs | $t(titles.app)",
"temporarydocs": "",
"timetickets": "",
"ttapprovals": "",
"vehicledetail": "Détails du véhicule {{vehicle} | $t(titles.app)",
"vehicles": "Tous les véhicules | $t(titles.app)"
},
"tt_approvals": {
"actions": {
"approveselected": ""
}
},
"user": {
"actions": {
"changepassword": "",

View File

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

View File

@@ -673,6 +673,33 @@
_eq: true
- exported:
_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:
name: bodyshops
schema: public
@@ -831,6 +858,13 @@
table:
name: transitions
schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: tt_approval_queue
schema: public
- name: vehicles
using:
foreign_key_constraint_on:
@@ -938,6 +972,7 @@
- tt_enforce_hours_for_tech_console
- updated_at
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- website
- workingdays
@@ -1027,6 +1062,7 @@
- tt_enforce_hours_for_tech_console
- updated_at
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- website
- workingdays
@@ -2262,6 +2298,13 @@
table:
name: timetickets
schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: employeeid
table:
name: tt_approval_queue
schema: public
insert_permissions:
- role: user
permission:
@@ -3145,6 +3188,13 @@
table:
name: transitions
schema: public
- name: tt_approval_queues
using:
foreign_key_constraint_on:
column: jobid
table:
name: tt_approval_queue
schema: public
insert_permissions:
- role: user
permission:
@@ -4033,6 +4083,41 @@
template_engine: Kriti
url: '{{$base_url}}/record-handler/arms'
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:
name: masterdata
schema: public
@@ -4457,6 +4542,32 @@
_eq: X-Hasura-User-Id
- active:
_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:
name: parts_order_lines
schema: public
@@ -4880,6 +4991,36 @@
_eq: X-Hasura-User-Id
- active:
_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:
name: phonebook
schema: public
@@ -5210,6 +5351,12 @@
- name: job
using:
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:
- role: user
permission:
@@ -5228,6 +5375,8 @@
- ciecacode
- clockoff
- clockon
- commited_by
- committed_at
- cost_center
- created_at
- date
@@ -5238,6 +5387,7 @@
- memo
- productivehrs
- rate
- ttapprovalqueueid
- updated_at
select_permissions:
- role: user
@@ -5248,6 +5398,8 @@
- ciecacode
- clockoff
- clockon
- commited_by
- committed_at
- cost_center
- created_at
- date
@@ -5258,6 +5410,7 @@
- memo
- productivehrs
- rate
- ttapprovalqueueid
- updated_at
filter:
bodyshop:
@@ -5277,6 +5430,8 @@
- ciecacode
- clockoff
- clockon
- commited_by
- committed_at
- cost_center
- created_at
- date
@@ -5287,6 +5442,7 @@
- memo
- productivehrs
- rate
- ttapprovalqueueid
- updated_at
filter:
bodyshop:
@@ -5397,6 +5553,119 @@
authid:
_eq: X-Hasura-User-Id
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:
name: users
schema: public
@@ -5467,6 +5736,20 @@
table:
name: parts_orders
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:
- role: user
permission:
@@ -5652,6 +5935,34 @@
_eq: X-Hasura-User-Id
- active:
_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:
name: vendors
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",
"license": "UNLICENSED",
"dependencies": {
"@opensearch-project/opensearch": "^2.2.1",
"aws-sdk": "^2.1326.0",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
@@ -362,6 +363,22 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3066,6 +3083,14 @@
"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": {
"version": "2.0.2",
"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==",
"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": {
"version": "7.3.8",
"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",
"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": {
"version": "1.1.2",
"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",
"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": {
"version": "2.0.2",
"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",
"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": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",

View File

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

View File

@@ -264,6 +264,13 @@ app.post("/newlog", (req, res) => {
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");
app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default);

View File

@@ -78,7 +78,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state:
jobs_by_pk.state_tax_rate === 0
? false
@@ -93,7 +93,14 @@ exports.default = function ({
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
jobline: jobline,
type: "part",
job: jobs_by_pk,
})
: taxCodes[taxAccountCode];
if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = {
@@ -158,13 +165,16 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({ jobline: jobline, type: "labor" })
: taxCodes[taxAccountCode];
if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = {
@@ -225,13 +235,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "materials",
})
: taxCodes[taxAccountCode];
if (!invoiceLineHash[mapaAccount.name])
invoiceLineHash[mapaAccount.name] = {};
if (!invoiceLineHash[mapaAccount.name][QboTaxId]) {
@@ -292,13 +309,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "materials",
})
: taxCodes[taxAccountCode];
if (!invoiceLineHash[mashAccount.name])
invoiceLineHash[mashAccount.name] = {};
if (!invoiceLineHash[mashAccount.name][QboTaxId]) {
@@ -375,7 +399,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
@@ -383,7 +407,14 @@ exports.default = function ({
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "towing",
})
: taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
@@ -427,7 +458,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
@@ -435,7 +466,14 @@ exports.default = function ({
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "storage",
})
: taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
@@ -482,13 +520,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
type: "adjustment",
job: jobs_by_pk,
})
: taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
@@ -598,7 +643,7 @@ exports.default = function ({
findTaxCode(
{
local: false,
federal: true,
federal: process.env.COUNTRY === "USA" ? false : true,
state: false,
},
bodyshop.md_responsibility_centers.sales_tax_codes
@@ -622,29 +667,29 @@ exports.default = function ({
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_")
) {
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat(
DineroQbFormat
),
SalesItemLineDetail: {
...(jobs_by_pk.class
? { ClassRef: { value: classes[jobs_by_pk.class] } }
: {}),
ItemRef: {
value:
items[bodyshop.md_responsibility_centers.taxes.federal.accountitem],
},
Qty: 1,
},
});
}
// if (
// bodyshop.accountingconfig &&
// bodyshop.accountingconfig.qbo &&
// bodyshop.accountingconfig.qbo_usa &&
// bodyshop.region_config.includes("CA_")
// ) {
// InvoiceLineAdd.push({
// DetailType: "SalesItemLineDetail",
// Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat(
// DineroQbFormat
// ),
// SalesItemLineDetail: {
// ...(jobs_by_pk.class
// ? { ClassRef: { value: classes[jobs_by_pk.class] } }
// : {}),
// ItemRef: {
// value:
// items[bodyshop.md_responsibility_centers.taxes.federal.accountitem],
// },
// Qty: 1,
// },
// });
// }
if (!qbo && InvoiceLineAdd.length === 0) {
//Handle the scenario where there is a $0 sale invoice.
@@ -785,3 +830,19 @@ exports.createMultiQbPayerLines = function ({
return InvoiceLineAdd;
};
function CheckQBOUSATaxID({ jobline, job, type }) {
if (type === "labor") {
return jobline.lbr_tax ? "TAX" : "NON";
} else if (type === "part") {
return jobline.tax_part ? "TAX" : "NON";
} else if (type === "materials") {
return job.tax_paint_mat_rt > 0 ? "TAX" : "NON";
} else if (type === " towing") {
return true ? "TAX" : "NON";
} else if (type === "adjustment") {
return false ? "TAX" : "NON";
} else {
throw new Error(`Unknown type to calculate tax id: ${type} `);
}
}

View File

@@ -18,7 +18,6 @@ const {
const OAuthClient = require("intuit-oauth");
const CreateInvoiceLines = require("../qb-receivables-lines").default;
const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
@@ -635,8 +634,7 @@ async function InsertInvoice(
],
...(bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_") && {
bodyshop.accountingconfig.qbo_usa && {
TxnTaxDetail: {
TxnTaxCodeRef: {
value:

View File

@@ -124,9 +124,16 @@ const generateBillLine = (billLine, responsibilityCenters, jobClass) => {
.multiply(billLine.quantity || 1)
.toFormat(DineroQbFormat),
...(jobClass ? { ClassRef: { FullName: jobClass } } : {}),
SalesTaxCodeRef: {
FullName: findTaxCode(billLine, responsibilityCenters.sales_tax_codes),
},
...(process.env.COUNTRY !== "USA"
? {
SalesTaxCodeRef: {
FullName: findTaxCode(
billLine,
responsibilityCenters.sales_tax_codes
),
},
}
: {}),
};
};

View File

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

View File

@@ -225,6 +225,8 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
adjustment_bottom_line
state_tax_rate
qb_multiple_payers
tax_paint_mat_rt
tax_lbr_rt
owner {
accountingid
}
@@ -243,6 +245,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
prt_dsmk_p
prt_dsmk_m
tax_part
lbr_tax
line_ref
unq_seq
lbr_op
@@ -658,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) {
bodyshops_by_pk(id: $bodyshopid){
id
shopname
address1
@@ -674,6 +676,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
use_paint_scale_data
timezone
}
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
@@ -829,6 +832,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
employee_number
id
}
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
}
}
`;
@@ -1224,12 +1231,17 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
flat_rate
ciecacode
}
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
bodyshop{
id
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
use_paint_scale_data
}
}
}`;
@@ -1332,12 +1344,17 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
flat_rate
ciecacode
}
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
bodyshop {
id
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
pbs_serialnumber
use_paint_scale_data
}
}
}
@@ -1770,8 +1787,8 @@ exports.GET_JOB_FOR_PPC = `query GET_JOB_FOR_PPC($jobid: uuid!) {
timezone
}
}
`
`;
exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop {
@@ -1795,3 +1812,11 @@ exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCriti
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[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
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.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero({
amount: (job.mixdata[0] && job.mixdata[0].totalliquidcost * 100) || 0,
});
} 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)
);
}
} 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) {

View File

@@ -20,7 +20,8 @@ async function StatusTransition(req, res) {
res.status(403).send("Unauthorized");
return;
}
res.sendStatus(200);
return;
const {
id: jobid,
status: value,
@@ -78,7 +79,7 @@ async function StatusTransition(req, res) {
res.sendStatus(200); //.json(ret);
} 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,
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