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 BabelEdit project file
@@ -6898,6 +6898,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>enter</name> <name>enter</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -6963,6 +6984,53 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </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> <folder_node>
<name>users</name> <name>users</name>
<children> <children>
@@ -7246,6 +7314,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>la1</name> <name>la1</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -8932,6 +9042,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>use_paint_scale_data</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>uselocalmediaserver</name> <name>uselocalmediaserver</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -16464,6 +16595,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>excel</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>exceptiontitle</name> <name>exceptiontitle</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -32471,6 +32623,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>vehicles</name> <name>vehicles</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33634,6 +33807,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>savetojobnotes</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -37756,6 +37950,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dms_posting_sheet</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>envelope_return_address</name> <name>envelope_return_address</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -41409,6 +41624,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>exported_gsr_by_ro</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>exported_gsr_by_ro_labor</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>gsr_by_atp</name> <name>gsr_by_atp</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -44565,6 +44822,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>cost_center</name> <name>cost_center</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -44649,6 +44927,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>flat_rate</name> <name>flat_rate</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -46270,6 +46569,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>vehicle-details</name> <name>vehicle-details</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -47196,6 +47516,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>vehicledetail</name> <name>vehicledetail</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -47240,6 +47581,37 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </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> <folder_node>
<name>user</name> <name>user</name>
<children> <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 { .ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important; background: #eaeaea !important;
} }
.job-line-manual{ .job-line-manual {
color: tomato; color: tomato;
font-style: italic; font-style: italic;
} }
td.ant-table-column-sort {
background-color: transparent;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -532,6 +532,42 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
> >
<InputNumber /> <InputNumber />
</Form.Item> </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 <Form.Item
label={t("bodyshop.fields.rbac.shop.vendors")} label={t("bodyshop.fields.rbac.shop.vendors")}
rules={[ 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 ( return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}> <Sider
style={{
height: "100vh",
position: "sticky",
top: 0,
left: 0,
}}
collapsible
collapsed={collapsed}
onCollapse={onCollapse}
>
<Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline"> <Menu theme="dark" defaultSelectedKeys={["1"]} mode="inline">
<Menu.Item <Menu.Item
key="1" key="1"

View File

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

View File

@@ -10,6 +10,7 @@ import {
Space, Space,
Table, Table,
} from "antd"; } from "antd";
import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; 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 EmployeeTeamSearchSelectComponent from "../employee-team-search-select/employee-team-search-select.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.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 { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import TimeTicketsTasksPresets from "../time-ticket-tasks-presets/time-ticket-tasks-presets.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({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -39,7 +39,6 @@ export default connect(
export function TimeTicketTaskModalComponent({ export function TimeTicketTaskModalComponent({
bodyshop, bodyshop,
form, form,
employeeAutoCompleteOptions,
lineTicketCalled, lineTicketCalled,
calculateTimeTickets, calculateTimeTickets,
lineTicketLoading, lineTicketLoading,
@@ -72,18 +71,6 @@ export function TimeTicketTaskModalComponent({
/> />
</Form.Item> </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 <Form.Item
name="employeeteamid" name="employeeteamid"
label={t("timetickets.fields.employee_team")} label={t("timetickets.fields.employee_team")}
@@ -243,7 +230,13 @@ export function TimeTicketTaskModalComponent({
{errors.map((e, idx) => ( {errors.map((e, idx) => (
<Alert key={idx} message={e} /> <Alert key={idx} message={e} />
))} ))}
<div style={{ display: "none" }}> <div
style={
{
//display: "none"
}
}
>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Form.Item <Form.Item
key={field.key} key={field.key}
@@ -358,7 +351,7 @@ export function TimeTicketTaskModalComponent({
!lineTicketLoading) !lineTicketLoading)
) { ) {
queryJobInfo({ variables: { id: jobid } }).then(() => queryJobInfo({ variables: { id: jobid } }).then(() =>
calculateTimeTickets() calculateTimeTickets("")
); );
} }
return ( return (

View File

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

View File

@@ -15,6 +15,7 @@ export default function TimeTicketsTasksPresets({
percent: preset.percent, percent: preset.percent,
hourstype: preset.hourstype, hourstype: preset.hourstype,
}); });
calculateTimeTickets(preset.memo); 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 { vendor {
id id
name name
email
} }
order_date order_date
deliver_by deliver_by
@@ -104,6 +105,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
vendor { vendor {
id id
name name
email
} }
total total
invoice_number invoice_number

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,6 +59,81 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
} }
`; `;
export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
query QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE(
$employeeid: uuid!
$start: date!
$end: date!
$fixedStart: date!
$fixedEnd: date!
) {
timetickets(
where: {
date: { _gte: $start, _lte: $end }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
fixedperiod: timetickets(
where: {
date: { _gte: $fixedStart, _lte: $fixedEnd }
employeeid: { _eq: $employeeid }
}
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
clockoff
clockon
cost_center
created_at
date
id
rate
productivehrs
memo
jobid
flat_rate
job {
id
ro_number
}
employeeid
employee {
id
employee_number
first_name
last_name
}
}
}
`;
export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
query QUERY_TIME_TICKETS_IN_RANGE_SB( query QUERY_TIME_TICKETS_IN_RANGE_SB(
$start: date! $start: date!
@@ -83,6 +158,8 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
productivehrs productivehrs
memo memo
jobid jobid
committed_at
committed_by
flat_rate flat_rate
job { job {
id 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` export const UPDATE_TIME_TICKET = gql`
mutation UPDATE_TIME_TICKET( mutation UPDATE_TIME_TICKET(
$timeticketId: uuid! $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) } : {}), ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}),
order: [ order: [
{ {
[sortcolumn || "created_at"]: [sortcolumn || "ro_number"]:
sortorder && sortorder !== "false" sortorder && sortorder !== "false"
? sortorder === "descend" ? sortorder === "descend"
? "desc" ? "desc"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

237
os-loader.js Normal file
View File

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

52
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: state:
jobs_by_pk.state_tax_rate === 0 jobs_by_pk.state_tax_rate === 0
? false ? false
@@ -93,7 +93,14 @@ exports.default = function ({
bodyshop.md_responsibility_centers.sales_tax_codes 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]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) { if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = { invoiceLineHash[account.name][QboTaxId] = {
@@ -158,13 +165,16 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes 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]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) { if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = { invoiceLineHash[account.name][QboTaxId] = {
@@ -225,13 +235,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes 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]) if (!invoiceLineHash[mapaAccount.name])
invoiceLineHash[mapaAccount.name] = {}; invoiceLineHash[mapaAccount.name] = {};
if (!invoiceLineHash[mapaAccount.name][QboTaxId]) { if (!invoiceLineHash[mapaAccount.name][QboTaxId]) {
@@ -292,13 +309,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes 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]) if (!invoiceLineHash[mashAccount.name])
invoiceLineHash[mashAccount.name] = {}; invoiceLineHash[mashAccount.name] = {};
if (!invoiceLineHash[mashAccount.name][QboTaxId]) { if (!invoiceLineHash[mashAccount.name][QboTaxId]) {
@@ -375,7 +399,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes bodyshop.md_responsibility_centers.sales_tax_codes
@@ -383,7 +407,14 @@ exports.default = function ({
const account = responsibilityCenters.profits.find( const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"] (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({ InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail", DetailType: "SalesItemLineDetail",
Amount: Dinero({ Amount: Dinero({
@@ -427,7 +458,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes bodyshop.md_responsibility_centers.sales_tax_codes
@@ -435,7 +466,14 @@ exports.default = function ({
const account = responsibilityCenters.profits.find( const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"] (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({ InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail", DetailType: "SalesItemLineDetail",
Amount: Dinero({ Amount: Dinero({
@@ -482,13 +520,20 @@ exports.default = function ({
const taxAccountCode = findTaxCode( const taxAccountCode = findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true, state: jobs_by_pk.state_tax_rate === 0 ? false : true,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes 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({ InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail", DetailType: "SalesItemLineDetail",
Amount: Dinero({ Amount: Dinero({
@@ -598,7 +643,7 @@ exports.default = function ({
findTaxCode( findTaxCode(
{ {
local: false, local: false,
federal: true, federal: process.env.COUNTRY === "USA" ? false : true,
state: false, state: false,
}, },
bodyshop.md_responsibility_centers.sales_tax_codes bodyshop.md_responsibility_centers.sales_tax_codes
@@ -622,29 +667,29 @@ exports.default = function ({
//QB USA with GST //QB USA with GST
//This was required for the No. 1 Collision Group. //This was required for the No. 1 Collision Group.
if ( // if (
bodyshop.accountingconfig && // bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo && // bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa && // bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_") // bodyshop.region_config.includes("CA_")
) { // ) {
InvoiceLineAdd.push({ // InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail", // DetailType: "SalesItemLineDetail",
Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat( // Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat(
DineroQbFormat // DineroQbFormat
), // ),
SalesItemLineDetail: { // SalesItemLineDetail: {
...(jobs_by_pk.class // ...(jobs_by_pk.class
? { ClassRef: { value: classes[jobs_by_pk.class] } } // ? { ClassRef: { value: classes[jobs_by_pk.class] } }
: {}), // : {}),
ItemRef: { // ItemRef: {
value: // value:
items[bodyshop.md_responsibility_centers.taxes.federal.accountitem], // items[bodyshop.md_responsibility_centers.taxes.federal.accountitem],
}, // },
Qty: 1, // Qty: 1,
}, // },
}); // });
} // }
if (!qbo && InvoiceLineAdd.length === 0) { if (!qbo && InvoiceLineAdd.length === 0) {
//Handle the scenario where there is a $0 sale invoice. //Handle the scenario where there is a $0 sale invoice.
@@ -785,3 +830,19 @@ exports.createMultiQbPayerLines = function ({
return InvoiceLineAdd; 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 OAuthClient = require("intuit-oauth");
const CreateInvoiceLines = require("../qb-receivables-lines").default; const CreateInvoiceLines = require("../qb-receivables-lines").default;
const moment = require("moment-timezone"); const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient; const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils"); const { generateOwnerTier } = require("../qbxml/qbxml-utils");
const { createMultiQbPayerLines } = require("../qb-receivables-lines"); const { createMultiQbPayerLines } = require("../qb-receivables-lines");
@@ -635,8 +634,7 @@ async function InsertInvoice(
], ],
...(bodyshop.accountingconfig && ...(bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa && bodyshop.accountingconfig.qbo_usa && {
bodyshop.region_config.includes("CA_") && {
TxnTaxDetail: { TxnTaxDetail: {
TxnTaxCodeRef: { TxnTaxCodeRef: {
value: value:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2267
yarn.lock

File diff suppressed because it is too large Load Diff