Merged in release/2022-06-10 (pull request #501)

release/2022-06-10

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2022-06-06 21:34:08 +00:00
75 changed files with 2644 additions and 90 deletions

View File

@@ -5691,6 +5691,32 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>inventory</name>
<children>
<concept_node>
<name>list</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>jobs</name>
<children>
@@ -17213,6 +17239,241 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>inventory</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>addtoinventory</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>consumefrominventory</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>inserting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>consumedbyjob</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>frombillinvoicenumber</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>fromvendor</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>inventory</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>showall</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>showavailable</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>inserted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>joblines</name>
<children>
@@ -29934,6 +30195,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inventory</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</name>
<definition_loaded>false</definition_loaded>
@@ -40529,6 +40811,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>calendarperiod</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>dailyactual</name>
<definition_loaded>false</definition_loaded>
@@ -40571,6 +40874,69 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</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>lastmonth</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>lastweek</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>monthlytarget</name>
<definition_loaded>false</definition_loaded>
@@ -40592,6 +40958,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>productivestatistics</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>productivetimeticketsoverdate</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>targets</name>
<definition_loaded>false</definition_loaded>
@@ -40613,6 +41021,69 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>thismonth</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>thisweek</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>timetickets</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>todateactual</name>
<definition_loaded>false</definition_loaded>
@@ -40634,6 +41105,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>totaloverperiod</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>weeklyactual</name>
<definition_loaded>false</definition_loaded>
@@ -40769,37 +41261,6 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>scoredboard</name>
<children>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>updated</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>tech</name>
<children>
@@ -42383,6 +42844,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inventory</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</name>
<definition_loaded>false</definition_loaded>
@@ -43204,6 +43686,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inventory</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs</name>
<definition_loaded>false</definition_loaded>

View File

@@ -73,8 +73,8 @@ export function BillDetailEditcontainer({
sm: "100%",
md: "100%",
lg: "100%",
xl: "80%",
xxl: "80%",
xl: "90%",
xxl: "90%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]

View File

@@ -12,6 +12,7 @@ import {
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -50,6 +51,7 @@ function BillEnterModalContainer({
const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
@@ -79,8 +81,13 @@ function BillEnterModalContainer({
}
setLoading(true);
const { upload, location, outstanding_returns, ...remainingValues } =
values;
const {
upload,
location,
outstanding_returns,
inventory,
...remainingValues
} = values;
let adjustmentsToInsert = {};
@@ -190,6 +197,26 @@ function BillEnterModalContainer({
}
const billId = r1.data.insert_bills.returning[0].id;
const markInventoryConsumed =
inventory && inventory.filter((i) => i.consumefrominventory);
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
const r2 = await updateInventoryLines({
variables: {
InventoryIds: markInventoryConsumed.map((p) => p.id),
consumedbybillid: billId,
},
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("inventory.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
await Promise.all(
remainingValues.billlines

View File

@@ -48,6 +48,7 @@ export function BillFormComponent({
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -61,6 +62,7 @@ export function BillFormComponent({
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
@@ -86,7 +88,7 @@ export function BillFormComponent({
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId) {
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
@@ -95,12 +97,19 @@ export function BillFormComponent({
});
}
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
@@ -425,6 +434,7 @@ export function BillFormComponent({
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}

View File

@@ -8,6 +8,9 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -20,6 +23,12 @@ export function BillFormContainer({
disabled,
disableInvNumber,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
@@ -31,6 +40,8 @@ export function BillFormContainer({
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
useLazyQuery(QUERY_UNRECEIVED_LINES);
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] =
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
return (
<>
@@ -47,6 +58,7 @@ export function BillFormContainer({
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
loadInventory={loadInventory}
/>
{!billEdit && (
<BillCmdReturnsTableComponent
@@ -56,6 +68,14 @@ export function BillFormContainer({
returnData={returnData}
/>
)}
{Simple_Inventory.treatment === "on" && (
<BillInventoryTable
form={form}
inventoryLoading={inventoryLoading}
inventoryData={billEdit ? [] : inventoryData}
billEdit={billEdit}
/>
)}
</>
);
}

View File

@@ -8,7 +8,7 @@ import {
Space,
Switch,
Table,
Tooltip
Tooltip,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -18,6 +18,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -34,10 +36,16 @@ export function BillEnterModalLinesComponent({
discount,
form,
responsibilityCenters,
billEdit,
billid,
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const columns = (remove) => {
return [
{
@@ -477,9 +485,22 @@ export function BillEnterModalLinesComponent({
dataIndex: "actions",
render: (text, record) => (
<Button disabled={disabled} onClick={() => remove(record.name)}>
<DeleteFilled />
</Button>
<Space wrap>
<Button disabled={disabled} onClick={() => remove(record.name)}>
<DeleteFilled />
</Button>
<Form.Item shouldUpdate noStyle>
{() =>
Simple_Inventory.treatment === "on" && (
<BilllineAddInventory
disabled={!billEdit || form.isFieldsTouched()}
billline={getFieldValue("billlines")[record.fieldKey]}
jobid={getFieldValue("jobid")}
/>
)
}
</Form.Item>
</Space>
),
},
];

View File

@@ -0,0 +1,153 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
export function BillInventoryTable({
bodyshop,
form,
billEdit,
inventoryLoading,
inventoryData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (inventoryData) {
form.setFieldsValue({
inventory: inventoryData.inventory,
});
}
}, [inventoryData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
noStyle
>
{() => {
const is_inhouse =
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
if (!is_inhouse || billEdit) {
return null;
}
if (inventoryLoading) return <Skeleton />;
return (
<Form.List name="inventory">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("inventory.labels.inventory")}
</Typography.Title>
<table className="bill-inventory-table">
<thead>
<tr>
<th>{t("billlines.fields.line_desc")}</th>
<th>{t("vendors.fields.name")}</th>
<th>{t("billlines.fields.quantity")}</th>
<th>{t("billlines.fields.actual_price")}</th>
<th>{t("billlines.fields.actual_cost")}</th>
<th>{t("inventory.actions.consumefrominventory")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[
field.name,
"billline",
"bill",
"vendor",
"name",
]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "actual_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "actual_cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}consumefrominventory`}
name={[field.name, "consumefrominventory"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -0,0 +1,19 @@
.bill-inventory-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -0,0 +1,147 @@
import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd";
import { t } from "i18next";
import moment from "moment";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BilllineAddInventory);
export function BilllineAddInventory({
currentUser,
bodyshop,
billline,
disabled,
jobid,
}) {
const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => {
setLoading(true);
//Check to make sure there are no existing items already in the inventory.
const cm = {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
jobid: jobid,
isinhouse: true,
is_credit_memo: true,
date: moment().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
total: 0,
billlines: [
{
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
cost_center: billline.cost_center,
deductedfromlbr: billline.deductedfromlbr,
applicable_taxes: {
local: false, //billline.applicable_taxes.local,
state: false, //billline.applicable_taxes.state,
federal: false, // billline.applicable_taxes.federal,
},
},
],
};
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
const insertResult = await insertInventoryLine({
variables: {
joblineId: billline.joblineid,
joblineStatus: bodyshop.md_order_statuses.default_returned,
inv: {
shopid: bodyshop.id,
billlineid: billline.id,
actual_price: billline.actual_price,
actual_cost: billline.actual_cost,
quantity: billline.quantity,
line_desc: billline.line_desc,
},
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
pol: {
returnfrombill: billid,
vendorid: bodyshop.inhousevendorid,
deliver_by: moment().format("YYYY-MM-DD"),
parts_order_lines: {
data: [
{
line_desc: billline.line_desc,
act_price: billline.actual_price,
cost: billline.actual_cost,
quantity: billline.quantity,
job_line_id: billline.joblineid,
part_type: billline.jobline.part_type,
cm_received: true,
},
],
},
order_date: "2022-06-01",
orderedby: currentUser.email,
jobid: jobid,
user_email: currentUser.email,
return: true,
status: "Ordered",
},
},
refetchQueries: ["QUERY_BILL_BY_PK"],
});
if (!insertResult.errors) {
notification.open({
type: "success",
message: t("inventory.successes.inserted"),
});
} else {
notification.open({
type: "error",
message: t("inventory.errors.inserting", {
error: JSON.stringify(insertResult.errors),
}),
});
}
setLoading(false);
};
return (
<Tooltip title={t("inventory.actions.addtoinventory")}>
<Button
loading={loading}
disabled={disabled || billline?.inventories?.length > 0}
onClick={addToInventory}
>
<FileAddFilled />
</Button>
</Tooltip>
);
}

View File

@@ -1,12 +1,12 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table } from "antd";
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Tooltip } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import moment from "moment";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
@@ -56,7 +56,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
onFilter: (value, record) => value.includes(record.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status),
render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record;
const mileageOver = nextservicekm <= mileage;
const dueForService =
nextservicedate && moment(nextservicedate).isBefore(moment());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService) && (
<Tooltip title={t("contracts.labels.cardueforservice")}>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip>
)}
</Space>
);
},
},
{
title: t("courtesycars.fields.year"),

View File

@@ -1,3 +1,4 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, {
BankFilled,
BarChartOutlined,
@@ -83,6 +84,12 @@ function Header({
setReportCenterContext,
recentItems,
}) {
const { Simple_Inventory } = useTreatments(
["Simple_Inventory"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
return (
@@ -199,7 +206,20 @@ function Header({
>
{t("menus.header.enterbills")}
</Menu.Item>
<Menu.Divider key="div4" />
{Simple_Inventory.treatment === "on" && (
<>
<Menu.Divider key="div4" />
<Menu.Item
key="inventory"
icon={<Icon component={FaFileInvoiceDollar} />}
>
<Link to="/manage/inventory">
{t("menus.header.inventory")}
</Link>
</Menu.Item>
</>
)}
<Menu.Divider key="div7" />
<Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
@@ -216,7 +236,6 @@ function Header({
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Divider key="div5" />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">
{t("menus.header.timetickets")}
@@ -235,7 +254,6 @@ function Header({
{t("menus.header.entertimeticket")}
</Menu.Item>
<Menu.Divider key="div6" />
<Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")}

View File

@@ -0,0 +1,153 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
const search = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search;
const history = useHistory();
const { t } = useTranslation();
const columns = [
{
title: t("billlines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder: sortcolumn === "line_desc" && sortorder,
},
{
title: t("inventory.labels.frombillinvoicenumber"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => record.billline?.bill?.invoice_number,
},
{
title: t("inventory.labels.fromvendor"),
dataIndex: "vendorname",
key: "vendorname",
ellipsis: true,
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => record.billline?.bill?.vendor?.name,
},
{
title: t("billlines.fields.actual_price"),
dataIndex: "actual_price",
key: "actual_price",
render: (text, record) => (
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
),
},
{
title: t("billlines.fields.actual_cost"),
dataIndex: "actual_cost",
key: "actual_cost",
render: (text, record) => (
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
),
},
{
title: t("inventory.labels.consumedbyjob"),
dataIndex: "consumedbyjob",
key: "consumedbyjob",
ellipsis: true,
render: (text, record) => record.bill?.job?.ro_number,
},
];
const handleTableChange = (pagination, filters, sorter) => {
search.page = pagination.current;
search.sortcolumn = sorter.column && sorter.column.key;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
};
return (
<Card
extra={
<Space wrap>
{search.search && (
<>
<Typography.Title level={4}>
{t("general.labels.searchresults", { search: search.search })}
</Typography.Title>
<Button
onClick={() => {
delete search.search;
history.push({ search: queryString.stringify(search) });
}}
>
{t("general.actions.clear")}
</Button>
</>
)}
<Button
onClick={() => {
if (search.showall) delete search.showall;
else {
search.showall = true;
}
history.push({ search: queryString.stringify(search) });
}}
>
{search.showall
? t("inventory.labels.showavailable")
: t("inventory.labels.showall")}
</Button>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={search.search || t("general.labels.search")}
onSearch={(value) => {
search.search = value;
history.push({ search: queryString.stringify(search) });
}}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: total,
}}
columns={columns}
rowKey="id"
dataSource={jobs}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);

View File

@@ -0,0 +1,67 @@
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_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import AlertComponent from "../alert/alert.component";
import InventoryListPaginated from "./inventory-list.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
//bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search, showall } = searchParams;
const { loading, error, data, refetch } = useQuery(
QUERY_INVENTORY_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0,
limit: 25,
consumedIsNull: showall === "true" ? null : true,
order: [
{
[sortcolumn || "created_at"]:
sortorder && sortorder !== "false"
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
}
);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<RbacWrapper action="jobs:list-all">
<InventoryListPaginated
refetch={refetch}
loading={loading}
searchParams={searchParams}
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
jobs={data ? data.search_inventory : []}
/>
</RbacWrapper>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);

View File

@@ -86,6 +86,7 @@ export default function JobBillsTotalComponent({
const totalPartsSublet = Dinero(totals.parts.parts.total)
.add(Dinero(totals.parts.sublets.total))
.add(Dinero(totals.additional.shipping))
.add(Dinero(totals.additional.towing));
const discrepancy = totalPartsSublet.subtract(billTotals);

View File

@@ -22,7 +22,8 @@ export default function JobReconciliationModalComponent({ job, bills }) {
(j.part_type !== null && j.part_type !== "PAE") ||
(j.line_desc &&
j.line_desc.toLowerCase().includes("towing") &&
j.lbr_op === "OP13")
j.lbr_op === "OP13") ||
j.db_ref === "936004" //ADD SHIPPING LINE.
);
return (

View File

@@ -1,5 +1,6 @@
import i18next from "i18next";
import _ from "lodash";
export const reconcileByAssocLine = (
jobLines,
jobLineState,
@@ -73,7 +74,12 @@ export const reconcileByPrice = (
jobLines.forEach((jl) => {
const matchingBillLineIds = billLines
.filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed)
.filter(
(bl) =>
bl.actual_price === jl.act_price &&
bl.quantity === jl.part_qty &&
!jl.removed
)
.map((bl) => bl.id);
if (matchingBillLineIds.length > 1) {

View File

@@ -25,7 +25,7 @@ const ret = {
"jobs:detail": 1,
"jobs:partsqueue": 4,
"jobs:checklist-view": 2,
"jobs:list-ready": 1,
"bills:enter": 2,
"bills:view": 2,
"bills:list": 2,
@@ -66,5 +66,7 @@ const ret = {
"timetickets:shiftedit": 5,
"users:editaccess": 4,
"inventory:list": 1,
};
export default ret;

View File

@@ -26,7 +26,7 @@ export default function ScoreboardEntryEdit({ entry }) {
return;
} else {
notification["success"]({
message: t("scoredboard.successes.updated"),
message: t("scoreboard.successes.updated"),
});
setVisible(false);
}

View File

@@ -47,3 +47,15 @@ export const ListOfDaysInCurrentMonth = () => {
days.push(dateEnd.format("YYYY-MM-DD"));
return days;
};
export const ListDaysBetween = ({ start, end }) => {
const days = [];
const dateStart = moment(start);
const dateEnd = moment(end);
while (dateEnd.diff(dateStart, "days") > 0) {
days.push(dateStart.format("YYYY-MM-DD"));
dateStart.add(1, "days");
}
days.push(dateEnd.format("YYYY-MM-DD"));
return days;
};

View File

@@ -0,0 +1,81 @@
import { Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import {
Bar,
CartesianGrid,
ComposedChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component";
const graphProps = {
strokeWidth: 3,
};
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardTicketsBar);
export function ScoreboardTicketsBar({ data, bodyshop }) {
const { t } = useTranslation();
return (
<Card
title={t("scoreboard.labels.productivetimeticketsoverdate")}
extra={<TimeTicketsDatesSelector />}
>
<ResponsiveContainer width="100%" height={475}>
<ComposedChart
data={data.chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis strokeWidth={graphProps.strokeWidth} />
<Tooltip />
<Legend />
{/* <Area
type="monotone"
name="Accumulated Hours"
dataKey="accHrs"
fill="lightgreen"
stroke="green"
/> */}
{data &&
data.employees.map((e, idx) => (
<Bar
key={`${e}productive`}
name={e}
dataKey={`employees.${e}.productive`}
stackId="productive"
// barSize={20}
fill={data.colors[idx]}
/>
))}
{/* <Line
name="Target Hours"
type="monotone"
dataKey="accTargetHrs"
stroke="#ff7300"
strokeWidth={graphProps.strokeWidth}
/> */}
</ComposedChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,301 @@
import { useQuery } from "@apollo/client";
import { Col, Row } from "antd";
import _ from "lodash";
import moment from "moment";
import queryString from "query-string";
import React, { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import ScoreboardTicketsBar from "./scoreboard-timetickets.bar.component";
import ScoreboardTicketsStats from "./scoreboard-timetickets.stats.component";
export default function ScoreboardTimeTickets() {
const searchParams = queryString.parse(useLocation().search);
const { start, end } = searchParams;
const startDate = start
? moment(start)
: moment().startOf("week").subtract(7, "days");
const endDate = end ? moment(end) : moment().endOf("week");
const fixedPeriods = useMemo(() => {
const endOfThisMonth = moment().endOf("month");
const startofthisMonth = moment().startOf("month");
const endOfLastmonth = moment().subtract(1, "month").endOf("month");
const startOfLastmonth = moment().subtract(1, "month").startOf("month");
const endOfThisWeek = moment().endOf("week");
const startOfThisWeek = moment().startOf("week");
const endOfLastWeek = moment().subtract(1, "week").endOf("week");
const startOfLastWeek = moment().subtract(1, "week").startOf("week");
const allDates = [
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
];
const start = moment.min(allDates);
const end = moment.max(allDates);
return {
start,
end,
endOfThisMonth,
startofthisMonth,
endOfLastmonth,
startOfLastmonth,
endOfThisWeek,
startOfThisWeek,
endOfLastWeek,
startOfLastWeek,
};
}, []);
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE, {
variables: {
start: startDate.format("YYYY-MM-DD"),
end: endDate.format("YYYY-MM-DD"),
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
skip: !fixedPeriods,
});
const calculatedData = useMemo(() => {
if (!data) return [];
const ret = {
totalThisWeek: 0,
totalLastWeek: 0,
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
employees: {},
};
data.fixedperiod.forEach((ticket) => {
const ticketDate = moment(ticket.date);
if (!ret.employees[ticket.employee.employee_number]) {
ret.employees[ticket.employee.employee_number] = {
totalThisWeek: 0,
totalLastWeek: 0,
totalThisMonth: 0,
totalLastMonth: 0,
totalOverPeriod: 0,
};
}
if (
ticketDate.isBetween(
fixedPeriods.startOfThisWeek,
fixedPeriods.endOfThisWeek,
undefined,
"[]"
)
) {
ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs;
ret.employees[ticket.employee.employee_number].totalThisWeek =
ret.employees[ticket.employee.employee_number].totalThisWeek +
ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastWeek,
fixedPeriods.endOfLastWeek,
undefined,
"[]"
)
) {
ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs;
ret.employees[ticket.employee.employee_number].totalLastWeek =
ret.employees[ticket.employee.employee_number].totalLastWeek +
ticket.productivehrs;
}
if (
ticketDate.isBetween(
fixedPeriods.startofthisMonth,
fixedPeriods.endOfThisMonth,
undefined,
"[]"
)
) {
ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs;
ret.employees[ticket.employee.employee_number].totalThisMonth =
ret.employees[ticket.employee.employee_number].totalThisMonth +
ticket.productivehrs;
} else if (
ticketDate.isBetween(
fixedPeriods.startOfLastmonth,
fixedPeriods.endOfLastmonth,
undefined,
"[]"
)
) {
ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs;
ret.employees[ticket.employee.employee_number].totalLastMonth =
ret.employees[ticket.employee.employee_number].totalLastMonth +
ticket.productivehrs;
}
});
const ticketsGroupedByDate = _.groupBy(data.timetickets, "date");
const listOfDays = Utils.ListDaysBetween({
start: startDate,
end: endDate,
});
const employees = [];
const ret2 = [];
let totals = {
totalproductive: 0,
totalactual: 0,
employees: {},
};
listOfDays.forEach((day) => {
const r = {
date: moment(day).format("MM/DD"),
actualtotal: 0,
productivetotal: 0,
employees: {},
};
if (ticketsGroupedByDate[day]) {
ticketsGroupedByDate[day].forEach((ticket) => {
r.actualtotal = r.actualtotal + ticket.actualhrs;
r.productivetotal = r.productivetotal + ticket.productivehrs;
totals.totalactual = totals.totalactual + ticket.actualhrs;
totals.totalproductive =
totals.totalproductive + ticket.productivehrs;
employees.push(ticket.employee.employee_number);
//Add to table data.
ret.employees[ticket.employee.employee_number].totalOverPeriod =
ret.employees[ticket.employee.employee_number].totalOverPeriod +
ticket.productivehrs;
if (!totals.employees[ticket.employee.employee_number])
totals.employees[ticket.employee.employee_number] = {
totalactual: 0,
totalproductive: 0,
};
if (!r.employees[ticket.employee.employee_number])
r.employees[ticket.employee.employee_number] = {
actual: 0,
productive: 0,
};
//Add to totals.
totals.employees[ticket.employee.employee_number].totalproductive =
totals.employees[ticket.employee.employee_number].totalproductive +
ticket.productivehrs;
totals.employees[ticket.employee.employee_number].totalactual =
totals.employees[ticket.employee.employee_number].totalactual +
ticket.actualhrs;
//Add to dailys.
r.employees[ticket.employee.employee_number].productive =
r.employees[ticket.employee.employee_number].productive +
ticket.productivehrs;
r.employees[ticket.employee.employee_number].actual =
r.employees[ticket.employee.employee_number].actual +
ticket.actualhrs;
});
}
ret2.push(r);
});
return {
fixed: ret,
timeperiod: {
totals,
chartData: ret2,
employees: _.uniq(employees),
colors: getColorArray(employees.length),
},
};
}, [fixedPeriods, data, startDate, endDate]);
if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<ScoreboardTicketsStats data={calculatedData.fixed} />
</Col>
<Col span={24}>
<ScoreboardTicketsBar
start={startDate}
end={endDate}
data={calculatedData.timeperiod}
/>
</Col>
</Row>
);
}
//Include a filter by employee.
//Hours produced today.
//Hours produced in last 7 days
//Hours produced for time period by day
//Hours produced by employee by day for time period.
function getColorArray(num) {
return [
"#3366cc",
"#dc3912",
"#ff9900",
"#109618",
"#990099",
"#0099c6",
"#dd4477",
"#66aa00",
"#b82e2e",
"#316395",
"#3366cc",
"#994499",
"#22aa99",
"#aaaa11",
"#6633cc",
"#e67300",
"#8b0707",
"#651067",
"#329262",
"#5574a6",
"#3b3eac",
"#b77322",
"#16d620",
"#b91383",
"#f4359e",
"#9c5935",
"#a9c413",
"#2a778d",
"#668d1c",
"#bea413",
"#0c5922",
"#743411",
];
// var result = [];
// for (var i = 0; i < num; i += 1) {
// var letters = "0123456789ABCDEF".split("");
// var color = "#";
// for (var j = 0; j < 6; j += 1) {
// color += letters[Math.floor(Math.random() * 16)];
// }
// result.push(color);
// }
// return result;
}

View File

@@ -0,0 +1,115 @@
import { Card, Col, Row, Statistic, Table, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardTicketsStats);
export function ScoreboardTicketsStats({ data, bodyshop }) {
const { t } = useTranslation();
const columns = [
{
title: t("employees.fields.employee_number"),
dataIndex: "employee_number",
key: "employee_number",
sorter: (a, b) => a.employee_number - b.employee_number,
},
{
title: t("scoreboard.labels.thisweek"),
dataIndex: "totalThisWeek",
key: "totalThisWeek",
sorter: (a, b) => a.totalThisWeek - b.totalThisWeek,
},
{
title: t("scoreboard.labels.lastweek"),
dataIndex: "totalLastWeek",
key: "totalLastWeek",
sorter: (a, b) => a.totalLastWeek - b.totalLastWeek,
},
{
title: t("scoreboard.labels.thismonth"),
dataIndex: "totalThisMonth",
key: "totalThisMonth",
sorter: (a, b) => a.totalThisMonth - b.totalThisMonth,
},
{
title: t("scoreboard.labels.lastmonth"),
dataIndex: "totalLastMonth",
key: "totalLastMonth",
sorter: (a, b) => a.totalLastMonth - b.totalLastMonth,
},
{
title: t("scoreboard.labels.totaloverperiod"),
dataIndex: "totalOverPeriod",
key: "totalOverPeriod",
sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod,
},
];
const tableData = data
? Object.keys(data.employees).map((key) => {
return { employee_number: key, ...data.employees[key] };
})
: [];
return (
<Card title={t("scoreboard.labels.productivestatistics")}>
<Row gutter={[16, 16]}>
<Col md={24} lg={4}>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={t("scoreboard.labels.lastweek")}
value={data.totalLastWeek.toFixed(1)}
/>
</Col>
<Col span={12}>
<Statistic
title={t("scoreboard.labels.lastmonth")}
value={data.totalLastMonth.toFixed(1)}
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Statistic
title={t("scoreboard.labels.thisweek")}
value={data.totalThisWeek.toFixed(1)}
/>
</Col>
<Col span={12}>
<Statistic
title={t("scoreboard.labels.thismonth")}
value={data.totalThisMonth.toFixed(1)}
/>
</Col>
</Row>
<Typography.Text type="secondary">
{t("scoreboard.labels.calendarperiod")}
</Typography.Text>
</Col>
<Col md={24} lg={20}>
<Table
columns={columns}
dataSource={tableData}
id="employee_number"
scroll={{ y: "300px" }}
/>
</Col>
</Row>
</Card>
);
}

View File

@@ -633,6 +633,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.inventory.list")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "inventory:list"]}
>
<InputNumber />
</Form.Item>
</LayoutFormRow>
</RbacWrapper>
);

View File

@@ -11,13 +11,21 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import TechClockInComponent from "./tech-job-clock-in-form.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import moment from "moment";
import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
bodyshop: selectBodyshop,
});
export function TechClockInContainer({ technician, bodyshop }) {
const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
});
export function TechClockInContainer({
setTimeTicketContext,
technician,
bodyshop,
}) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET, {
@@ -75,6 +83,16 @@ export function TechClockInContainer({ technician, bodyshop }) {
title={t("timetickets.labels.clockintojob")}
extra={
<Space wrap>
<Button
onClick={() => {
setTimeTicketContext({
actions: {},
context: { timeticket: { employeeid: technician.id } },
});
}}
>
{t("timetickets.actions.enter")}
</Button>
<TechJobPrintTickets />
<Button
type="primary"
@@ -92,4 +110,7 @@ export function TechClockInContainer({ technician, bodyshop }) {
</Card>
);
}
export default connect(mapStateToProps, null)(TechClockInContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TechClockInContainer);

View File

@@ -17,6 +17,7 @@ export default function TimeTicketsDatesSelector() {
if (!!start && !!end) {
history.push({
search: queryString.stringify({
...searchParams,
start: start.format("YYYY-MM-DD"),
end: end.format("YYYY-MM-DD"),
}),
@@ -25,6 +26,7 @@ export default function TimeTicketsDatesSelector() {
} else {
history.push({
search: queryString.stringify({
...searchParams,
start: null,
end: null,
}),

View File

@@ -35,6 +35,7 @@ export function TimeTicketModalComponent({
authLevel,
employeeAutoCompleteOptions,
isEdit,
employeeSelectDisabled,
}) {
const { t } = useTranslation();
@@ -118,6 +119,7 @@ export function TimeTicketModalComponent({
]}
>
<EmployeeSearchSelect
disabled={employeeSelectDisabled}
options={employeeAutoCompleteOptions}
onSelect={(value) => {
const emps =

View File

@@ -244,6 +244,12 @@ export function TimeTicketModalContainer({
employeeAutoCompleteOptions={
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
}
employeeSelectDisabled={
timeTicketModal.context?.timeticket?.employeeid &&
!timeTicketModal.context.id
? true
: false
}
/>
</Form>
</Modal>

View File

@@ -152,6 +152,10 @@ export const QUERY_BILL_BY_PK = gql`
state_tax_rate
federal_tax_rate
isinhouse
inventories {
id
line_desc
}
vendor {
id
name
@@ -165,6 +169,9 @@ export const QUERY_BILL_BY_PK = gql`
cost_center
quantity
joblineid
inventories {
id
}
jobline {
oem_partno
part_type

View File

@@ -80,6 +80,7 @@ export const QUERY_ALL_CC = gql`
status
vin
year
mileage
cccontracts(
where: { status: { _eq: "contracts.status.out" } }
order_by: { contract_date: desc }

View File

@@ -0,0 +1,112 @@
import { gql } from "@apollo/client";
export const INSERT_INVENTORY_AND_CREDIT = gql`
mutation INSERT_INVENTORY_AND_CREDIT(
$inv: inventory_insert_input!
$cm: bills_insert_input!
$pol: parts_orders_insert_input!
$joblineId: uuid!
$joblineStatus: String
) {
insert_inventory_one(object: $inv) {
id
}
insert_bills_one(object: $cm) {
id
}
insert_parts_orders_one(object: $pol) {
id
}
update_joblines_by_pk(
pk_columns: { id: $joblineId }
_set: { status: $joblineStatus }
) {
id
status
}
}
`;
export const UPDATE_INVENTORY_LINES = gql`
mutation UPDATE_INVENTORY_LINES(
$InventoryIds: [uuid!]!
$consumedbybillid: uuid!
) {
update_inventory(
where: { id: { _in: $InventoryIds } }
_set: { consumedbybillid: $consumedbybillid }
) {
affected_rows
}
}
`;
export const QUERY_OUTSTANDING_INVENTORY = gql`
query QUERY_OUTSTANDING_INVENTORY {
inventory(where: { consumedbybillid: { _is_null: true } }) {
id
actual_cost
actual_price
quantity
billlineid
line_desc
billline {
bill {
invoice_number
vendor {
name
}
}
}
}
}
`;
export const QUERY_INVENTORY_PAGINATED = gql`
query QUERY_INVENTORY_PAGINATED(
$search: String
$offset: Int
$limit: Int
$order: [inventory_order_by!]
$consumedIsNull: Boolean
) {
search_inventory(
args: { search: $search }
offset: $offset
limit: $limit
order_by: $order
where: { consumedbybillid: { _is_null: $consumedIsNull } }
) {
id
line_desc
actual_price
actual_cost
bill {
id
invoice_number
job {
ro_number
id
}
}
billline {
id
bill {
id
invoice_number
vendor {
id
name
}
}
}
}
search_inventory_aggregate(
args: { search: $search }
where: { consumedbybillid: { _is_null: $consumedIsNull } }
) {
aggregate {
count(distinct: true)
}
}
}
`;

View File

@@ -61,6 +61,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
flat_rate
clockon
clockoff
rate
employee {
id
first_name

View File

@@ -26,7 +26,12 @@ export const QUERY_TICKETS_BY_JOBID = gql`
`;
export const QUERY_TIME_TICKETS_IN_RANGE = gql`
query QUERY_TIME_TICKETS_IN_RANGE($start: date!, $end: date!) {
query QUERY_TIME_TICKETS_IN_RANGE(
$start: date!
$end: date!
$fixedStart: date!
$fixedEnd: date!
) {
timetickets(
where: { date: { _gte: $start, _lte: $end } }
order_by: { date: desc_nulls_first }
@@ -56,6 +61,35 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
last_name
}
}
fixedperiod: timetickets(
where: { date: { _gte: $fixedStart, _lte: $fixedEnd } }
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
}
}
}
`;

View File

@@ -0,0 +1,32 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import InventoryList from "../../components/inventory-list/inventory-list.container";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function InventoryPage({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.inventory");
setSelectedHeader("inventory");
setBreadcrumbs([{ link: "/manage/jobs", label: t("titles.bc.inventory") }]);
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<RbacWrapper action="inventory:list">
<InventoryList />
</RbacWrapper>
);
}
export default connect(null, mapDispatchToProps)(InventoryPage);

View File

@@ -34,6 +34,7 @@ const JobsPage = lazy(() => import("../jobs/jobs.page"));
const JobsDetailPage = lazy(() =>
import("../jobs-detail/jobs-detail.page.container")
);
const InventoryListPage = lazy(() => import("../inventory/inventory.page"));
const ProfilePage = lazy(() => import("../profile/profile.container.page"));
const JobsAvailablePage = lazy(() =>
import("../jobs-available/jobs-available.page.container")
@@ -250,6 +251,11 @@ export function Manage({ match, conflict, bodyshop }) {
<Route path={`${match.path}/jobs/:jobId`} component={JobsDetailPage} />
</Switch>
<Route exact path={`${match.path}/temporarydocs/`} component={TempDocs} />
<Route
exact
path={`${match.path}/inventory/`}
component={InventoryListPage}
/>
<Route
exact
path={`${match.path}/courtesycars/`}

View File

@@ -1,7 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table } from "antd";
import _ from "lodash";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -92,13 +91,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
// searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
if (filters.status) {
searchParams.statusFilters = JSON.stringify(
_.flattenDeep(filters.status)
);
} else {
delete searchParams.statusFilters;
}
history.push({ search: queryString.stringify(searchParams) });
};
@@ -244,6 +237,17 @@ export function PartsQueuePageComponent({ bodyshop }) {
key: "queued_for_parts",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
filters: [
{
text: "Queued",
value: true,
},
{
text: "Unqueued",
value: false,
},
],
//onFilter: (value, record) => record.queued_for_parts === value,
render: (text, record) => (
<JobRemoveFromPartsQueue
checked={record.queued_for_parts}

View File

@@ -1,15 +1,21 @@
import Icon, { BarsOutlined } from "@ant-design/icons";
import { Tabs } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { FaShieldAlt } from "react-icons/fa";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import ScoreboardDisplay from "../../components/scoreboard-display/scoreboard-display.component";
import ScoreboardTimeTickets from "../../components/scoreboard-timetickets/scoreboard-timetickets.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import queryString from "query-string";
import { useHistory, useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -22,7 +28,9 @@ const mapDispatchToProps = (dispatch) => ({
export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const { tab } = searchParams;
const history = useHistory();
useEffect(() => {
document.title = t("titles.scoreboard");
setSelectedHeader("scoreboard");
@@ -37,7 +45,41 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
return (
<FeatureWrapper featureName="scoreboard">
<RbacWrapper action="scoreboard:view">
<ScoreboardDisplay />
<Tabs
activeKey={tab || "sb"}
destroyInactiveTabPane
onChange={(key) => {
searchParams.tab = key;
history.push({
search: queryString.stringify(searchParams),
});
}}
>
<Tabs.TabPane
tab={
<span>
<Icon component={FaShieldAlt} />
{t("scoreboard.labels.jobs")}
</span>
}
destroyInactiveTabPane
key="sb"
>
<ScoreboardDisplay />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<BarsOutlined />
{t("scoreboard.labels.timetickets")}
</span>
}
destroyInactiveTabPane
key="tickets"
>
<ScoreboardTimeTickets />
</Tabs.TabPane>
</Tabs>
</RbacWrapper>
</FeatureWrapper>
);

View File

@@ -352,6 +352,9 @@
"employees": {
"page": "Employees -> List"
},
"inventory": {
"list": "Inventory -> List"
},
"jobs": {
"admin": "Jobs -> Admin",
"available-list": "Jobs -> Available List",
@@ -1069,6 +1072,26 @@
"printpack": "Intake Print Pack"
}
},
"inventory": {
"actions": {
"addtoinventory": "Add to Inventory",
"consumefrominventory": "Consume from Inventory?"
},
"errors": {
"inserting": "Error inserting inventory item. {{error}}"
},
"labels": {
"consumedbyjob": "Consumed by Job",
"frombillinvoicenumber": "Original Bill Invoice Number",
"fromvendor": "Original Bill Vendor",
"inventory": "Inventory",
"showall": "Show All Inventory",
"showavailable": "Show Only Available Inventory"
},
"successes": {
"inserted": "Added line to inventory."
}
},
"joblines": {
"actions": {
"new": "New Line"
@@ -1754,6 +1777,7 @@
"export-logs": "Export Logs",
"help": "Help",
"home": "Home",
"inventory": "Inventory",
"jobs": "Jobs",
"newjob": "Create New Job",
"owners": "Owners",
@@ -2403,11 +2427,21 @@
},
"labels": {
"asoftodaytarget": "As of Today",
"calendarperiod": "Periods based on calendar weeks/months.",
"dailyactual": "Actual (D)",
"dailytarget": "Daily",
"jobs": "Jobs",
"lastmonth": "Last Month",
"lastweek": "Last Week",
"monthlytarget": "Monthly",
"productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"targets": "Targets",
"thismonth": "This Month",
"thisweek": "This Week",
"timetickets": "Timetickets",
"todateactual": "Actual (MTD)",
"totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)",
"weeklytarget": "Weekly",
"workingdays": "Working Days / Month"
@@ -2418,11 +2452,6 @@
"updated": "Scoreboard updated."
}
},
"scoredboard": {
"successes": {
"updated": "Scoreboard entry updated."
}
},
"tech": {
"fields": {
"employeeid": "Employee ID",
@@ -2523,6 +2552,7 @@
"dashboard": "Dashboard",
"dms": "DMS Export",
"export-logs": "Export Logs",
"inventory": "Inventory",
"jobs": "Jobs",
"jobs-active": "Active Jobs",
"jobs-admin": "Admin",
@@ -2563,6 +2593,7 @@
"dashboard": "Dashboard | $t(titles.app)",
"dms": "DMS Export | $t(titles.app)",
"export-logs": "Export Logs | $t(titles.app)",
"inventory": "Inventory | $t(titles.app)",
"jobs": "Active Jobs | $t(titles.app)",
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",
"jobs-all": "All Jobs | $t(titles.app)",

View File

@@ -352,6 +352,9 @@
"employees": {
"page": ""
},
"inventory": {
"list": ""
},
"jobs": {
"admin": "",
"available-list": "",
@@ -1069,6 +1072,26 @@
"printpack": ""
}
},
"inventory": {
"actions": {
"addtoinventory": "",
"consumefrominventory": ""
},
"errors": {
"inserting": ""
},
"labels": {
"consumedbyjob": "",
"frombillinvoicenumber": "",
"fromvendor": "",
"inventory": "",
"showall": "",
"showavailable": ""
},
"successes": {
"inserted": ""
}
},
"joblines": {
"actions": {
"new": ""
@@ -1754,6 +1777,7 @@
"export-logs": "",
"help": "",
"home": "Casa",
"inventory": "",
"jobs": "Trabajos",
"newjob": "",
"owners": "propietarios",
@@ -2403,11 +2427,21 @@
},
"labels": {
"asoftodaytarget": "",
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
"monthlytarget": "",
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"todateactual": "",
"totaloverperiod": "",
"weeklyactual": "",
"weeklytarget": "",
"workingdays": ""
@@ -2418,11 +2452,6 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",
@@ -2523,6 +2552,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "",
"jobs-active": "",
"jobs-admin": "",
@@ -2563,6 +2593,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "",
"jobs-all": "",

View File

@@ -352,6 +352,9 @@
"employees": {
"page": ""
},
"inventory": {
"list": ""
},
"jobs": {
"admin": "",
"available-list": "",
@@ -1069,6 +1072,26 @@
"printpack": ""
}
},
"inventory": {
"actions": {
"addtoinventory": "",
"consumefrominventory": ""
},
"errors": {
"inserting": ""
},
"labels": {
"consumedbyjob": "",
"frombillinvoicenumber": "",
"fromvendor": "",
"inventory": "",
"showall": "",
"showavailable": ""
},
"successes": {
"inserted": ""
}
},
"joblines": {
"actions": {
"new": ""
@@ -1754,6 +1777,7 @@
"export-logs": "",
"help": "",
"home": "Accueil",
"inventory": "",
"jobs": "Emplois",
"newjob": "",
"owners": "Propriétaires",
@@ -2403,11 +2427,21 @@
},
"labels": {
"asoftodaytarget": "",
"calendarperiod": "",
"dailyactual": "",
"dailytarget": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
"monthlytarget": "",
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"todateactual": "",
"totaloverperiod": "",
"weeklyactual": "",
"weeklytarget": "",
"workingdays": ""
@@ -2418,11 +2452,6 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",
@@ -2523,6 +2552,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "",
"jobs-active": "",
"jobs-admin": "",
@@ -2563,6 +2593,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "Tous les emplois | $t(titles.app)",
"jobs-admin": "",
"jobs-all": "",

View File

@@ -10,6 +10,9 @@
- function:
schema: public
name: search_exportlog
- function:
schema: public
name: search_inventory
- function:
schema: public
name: search_jobs

View File

@@ -402,6 +402,14 @@
- name: jobline
using:
foreign_key_constraint_on: joblineid
array_relationships:
- name: inventories
using:
foreign_key_constraint_on:
column: billlineid
table:
schema: public
name: inventory
insert_permissions:
- role: user
permission:
@@ -541,6 +549,13 @@
table:
schema: public
name: exportlog
- name: inventories
using:
foreign_key_constraint_on:
column: consumedbybillid
table:
schema: public
name: inventory
- name: parts_orders
using:
foreign_key_constraint_on:
@@ -751,6 +766,13 @@
table:
schema: public
name: exportlog
- name: inventories
using:
foreign_key_constraint_on:
column: shopid
table:
schema: public
name: inventory
- name: jobs
using:
foreign_key_constraint_on:
@@ -2112,6 +2134,97 @@
- active:
_eq: true
allow_aggregations: true
- table:
schema: public
name: inventory
object_relationships:
- name: bill
using:
foreign_key_constraint_on: consumedbybillid
- name: billline
using:
foreign_key_constraint_on: billlineid
- name: bodyshop
using:
foreign_key_constraint_on: shopid
- name: jobline
using:
foreign_key_constraint_on: joblineid
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- actual_cost
- actual_price
- billlineid
- consumedbybillid
- created_at
- id
- joblineid
- line_desc
- quantity
- shopid
- updated_at
backend_only: false
select_permissions:
- role: user
permission:
columns:
- actual_cost
- actual_price
- billlineid
- consumedbybillid
- created_at
- id
- joblineid
- line_desc
- quantity
- shopid
- updated_at
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:
columns:
- actual_cost
- actual_price
- billlineid
- consumedbybillid
- created_at
- id
- joblineid
- line_desc
- quantity
- shopid
- updated_at
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table:
schema: public
name: ioevents
@@ -2211,6 +2324,13 @@
table:
schema: public
name: billlines
- name: inventories
using:
foreign_key_constraint_on:
column: joblineid
table:
schema: public
name: inventory
- name: parts_order_lines
using:
foreign_key_constraint_on:

View File

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

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."inventory" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "shopid" uuid NOT NULL, "billid" uuid, "joblineid" uuid, "line_desc" text NOT NULL, "actual_price" numeric NOT NULL, "actual_cost" numeric NOT NULL, "quantity" numeric NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("joblineid") REFERENCES "public"."joblines"("id") 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_inventory_updated_at"
BEFORE UPDATE ON "public"."inventory"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_inventory_updated_at" ON "public"."inventory"
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"."inventory" drop constraint "inventory_billid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory"
add constraint "inventory_billid_fkey"
foreign key ("billid")
references "public"."billlines"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1 @@
alter table "public"."inventory" drop constraint "inventory_shopid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory"
add constraint "inventory_shopid_fkey"
foreign key ("shopid")
references "public"."bodyshops"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1 @@
alter table "public"."inventory" rename column "billlineid" to "billid";

View File

@@ -0,0 +1 @@
alter table "public"."inventory" rename column "billid" to "billlineid";

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"."inventory" add column "consumedbybillid" uuid
-- null;

View File

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

View File

@@ -0,0 +1 @@
alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory"
add constraint "inventory_consumedbybillid_fkey"
foreign key ("consumedbybillid")
references "public"."bills"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey",
add constraint "inventory_consumedbybillid_fkey"
foreign key ("shopid")
references "public"."bodyshops"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey",
add constraint "inventory_consumedbybillid_fkey"
foreign key ("consumedbybillid")
references "public"."bills"
("id") on update restrict on delete set null;

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey",
add constraint "inventory_consumedbybillid_fkey"
foreign key ("shopid")
references "public"."bodyshops"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey",
add constraint "inventory_consumedbybillid_fkey"
foreign key ("consumedbybillid")
references "public"."bills"
("id") on update cascade on delete set null;

View File

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

View File

@@ -0,0 +1,2 @@
CREATE INDEX "inventory_consumedbybillid" on
"public"."inventory" using btree ("consumedbybillid");

View File

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

View File

@@ -0,0 +1,2 @@
CREATE INDEX "inventory_shopididx" on
"public"."inventory" using btree ("shopid");

View File

@@ -0,0 +1,40 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.search_inventory (search text)
-- RETURNS SETOF inventory
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
-- IF search = '' THEN
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory;
-- ELSE
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory i,
-- billlines bl,
-- bills b,
-- vendors v
-- WHERE
-- i.billlineid = bl.id
-- AND bl.billid = b.id
-- AND b.vendorid = v.id
-- AND i.line_desc ILIKE '%' || search || '%'
-- OR b.invoice_number ILIKE '%' || search || '%'
-- OR v.name ILIKE '%' || search || '%'
-- ORDER BY
-- i.line_desc ILIKE '%' || search || '%'
-- OR NULL,
-- b.invoice_number ILIKE '%' || search || '%'
-- OR NULL,
-- v.name ILIKE '%' || search || '%'
-- OR NULL;
-- END IF;
-- END
-- $function$;

View File

@@ -0,0 +1,38 @@
CREATE OR REPLACE FUNCTION public.search_inventory (search text)
RETURNS SETOF inventory
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
IF search = '' THEN
RETURN query
SELECT
*
FROM
inventory;
ELSE
RETURN query
SELECT
*
FROM
inventory i,
billlines bl,
bills b,
vendors v
WHERE
i.billlineid = bl.id
AND bl.billid = b.id
AND b.vendorid = v.id
AND i.line_desc ILIKE '%' || search || '%'
OR b.invoice_number ILIKE '%' || search || '%'
OR v.name ILIKE '%' || search || '%'
ORDER BY
i.line_desc ILIKE '%' || search || '%'
OR NULL,
b.invoice_number ILIKE '%' || search || '%'
OR NULL,
v.name ILIKE '%' || search || '%'
OR NULL;
END IF;
END
$function$;

View File

@@ -0,0 +1,37 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.search_inventory (search text)
-- RETURNS SETOF inventory
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
-- IF search = '' THEN
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory;
-- ELSE
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory inner JOIN billlines ON inventory.billlineid = billlines.id
-- inner JOIN bills ON billlines.billid = bills.id
-- inner JOIN vendors ON bills.vendorid = vendors.id
--
-- WHERE
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR bills.invoice_number ILIKE '%' || search || '%'
-- OR vendors.name ILIKE '%' || search || '%'
-- ORDER BY
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR NULL,
-- bills.invoice_number ILIKE '%' || search || '%'
-- OR NULL,
-- vendors.name ILIKE '%' || search || '%'
-- OR NULL;
-- END IF;
-- END
-- $function$;

View File

@@ -0,0 +1,35 @@
CREATE OR REPLACE FUNCTION public.search_inventory (search text)
RETURNS SETOF inventory
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
IF search = '' THEN
RETURN query
SELECT
*
FROM
inventory;
ELSE
RETURN query
SELECT
*
FROM
inventory inner JOIN billlines ON inventory.billlineid = billlines.id
inner JOIN bills ON billlines.billid = bills.id
inner JOIN vendors ON bills.vendorid = vendors.id
WHERE
inventory.line_desc ILIKE '%' || search || '%'
OR bills.invoice_number ILIKE '%' || search || '%'
OR vendors.name ILIKE '%' || search || '%'
ORDER BY
inventory.line_desc ILIKE '%' || search || '%'
OR NULL,
bills.invoice_number ILIKE '%' || search || '%'
OR NULL,
vendors.name ILIKE '%' || search || '%'
OR NULL;
END IF;
END
$function$;

View File

@@ -0,0 +1,37 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.search_inventory (search text)
-- RETURNS SETOF inventory
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
-- IF search = '' THEN
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory;
-- ELSE
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory inner JOIN billlines ON inventory.billlineid = billlines.id
-- inner JOIN bills ON billlines.billid = bills.id
-- inner JOIN vendors ON bills.vendorid = vendors.id
--
-- WHERE
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR bills.invoice_number ILIKE '%' || search || '%'
-- OR vendors.name ILIKE '%' || search || '%'
-- ORDER BY
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR NULL,
-- bills.invoice_number ILIKE '%' || search || '%'
-- OR NULL,
-- vendors.name ILIKE '%' || search || '%'
-- OR NULL;
-- END IF;
-- END
-- $function$;

View File

@@ -0,0 +1,35 @@
CREATE OR REPLACE FUNCTION public.search_inventory (search text)
RETURNS SETOF inventory
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
IF search = '' THEN
RETURN query
SELECT
*
FROM
inventory;
ELSE
RETURN query
SELECT
*
FROM
inventory inner JOIN billlines ON inventory.billlineid = billlines.id
inner JOIN bills ON billlines.billid = bills.id
inner JOIN vendors ON bills.vendorid = vendors.id
WHERE
inventory.line_desc ILIKE '%' || search || '%'
OR bills.invoice_number ILIKE '%' || search || '%'
OR vendors.name ILIKE '%' || search || '%'
ORDER BY
inventory.line_desc ILIKE '%' || search || '%'
OR NULL,
bills.invoice_number ILIKE '%' || search || '%'
OR NULL,
vendors.name ILIKE '%' || search || '%'
OR NULL;
END IF;
END
$function$;

View File

@@ -0,0 +1,37 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE FUNCTION public.search_inventory (search text)
-- RETURNS SETOF inventory
-- LANGUAGE plpgsql
-- STABLE
-- AS $function$
-- BEGIN
-- IF search = '' THEN
-- RETURN query
-- SELECT
-- *
-- FROM
-- inventory;
-- ELSE
-- RETURN query
-- SELECT
-- inventory.*
-- FROM
-- inventory inner JOIN billlines ON inventory.billlineid = billlines.id
-- inner JOIN bills ON billlines.billid = bills.id
-- inner JOIN vendors ON bills.vendorid = vendors.id
--
-- WHERE
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR bills.invoice_number ILIKE '%' || search || '%'
-- OR vendors.name ILIKE '%' || search || '%'
-- ORDER BY
-- inventory.line_desc ILIKE '%' || search || '%'
-- OR NULL,
-- bills.invoice_number ILIKE '%' || search || '%'
-- OR NULL,
-- vendors.name ILIKE '%' || search || '%'
-- OR NULL;
-- END IF;
-- END
-- $function$;

View File

@@ -0,0 +1,35 @@
CREATE OR REPLACE FUNCTION public.search_inventory (search text)
RETURNS SETOF inventory
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
IF search = '' THEN
RETURN query
SELECT
*
FROM
inventory;
ELSE
RETURN query
SELECT
inventory.*
FROM
inventory inner JOIN billlines ON inventory.billlineid = billlines.id
inner JOIN bills ON billlines.billid = bills.id
inner JOIN vendors ON bills.vendorid = vendors.id
WHERE
inventory.line_desc ILIKE '%' || search || '%'
OR bills.invoice_number ILIKE '%' || search || '%'
OR vendors.name ILIKE '%' || search || '%'
ORDER BY
inventory.line_desc ILIKE '%' || search || '%'
OR NULL,
bills.invoice_number ILIKE '%' || search || '%'
OR NULL,
vendors.name ILIKE '%' || search || '%'
OR NULL;
END IF;
END
$function$;

View File

@@ -0,0 +1,3 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- create index inventory_linedescidx on inventory(line_desc);

View File

@@ -0,0 +1 @@
create index inventory_linedescidx on inventory(line_desc);

View File

@@ -211,7 +211,7 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
qbo_realmId,
"query",
`select * From Customer where DisplayName = '${StandardizeName(
job.ins_co_nm
job.ins_co_nm.trim()
)}'`
),
method: "POST",
@@ -239,7 +239,7 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
const insCo = bodyshop.md_ins_cos.find((i) => i.name === job.ins_co_nm);
const Customer = {
DisplayName: job.ins_co_nm,
DisplayName: job.ins_co_nm.trim(),
BillWithParent: true,
BillAddr: {
City: job.ownr_city,
@@ -269,6 +269,7 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
}
}
exports.InsertInsuranceCo = InsertInsuranceCo;
async function QueryOwner(oauthClient, qbo_realmId, req, job) {
const ownerName = generateOwnerTier(job, true, null);
const result = await oauthClient.makeApiCall({

View File

@@ -249,13 +249,13 @@ const generateInvoiceQbxml = (
)}:${generateJobTier(jobs_by_pk)}`
).trim(),
},
ARAccountRef: {
FullName: bodyshop.md_responsibility_centers.ar.accountname,
},
...(jobs_by_pk.class
? { ClassRef: { FullName: jobs_by_pk.class } }
: {}),
ARAccountRef: {
FullName: bodyshop.md_responsibility_centers.ar.accountname,
},
TxnDate: moment(jobs_by_pk.date_invoiced)
.tz(bodyshop.timezone)
.format("YYYY-MM-DD"),

View File

@@ -6,11 +6,11 @@ exports.addQbxmlHeader = addQbxmlHeader = (xml) => {
};
exports.generateSourceTier = (jobs_by_pk) => {
return jobs_by_pk.ins_co_nm && jobs_by_pk.ins_co_nm.trim();
return jobs_by_pk.ins_co_nm && jobs_by_pk.ins_co_nm.trim().replace(":", " ");
};
exports.generateJobTier = (jobs_by_pk) => {
return jobs_by_pk.ro_number && jobs_by_pk.ro_number.trim();
return jobs_by_pk.ro_number && jobs_by_pk.ro_number.trim().replace(":", " ");
};
exports.generateOwnerTier = (jobs_by_pk, isThreeTier, twotierpref) => {
@@ -24,7 +24,9 @@ exports.generateOwnerTier = (jobs_by_pk, isThreeTier, twotierpref) => {
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)} #${jobs_by_pk.owner.accountingid || ""}`
).trim();
)
.trim()
.replace(":", " ");
} else {
//What's the 2 tier pref?
if (twotierpref === "source") {
@@ -40,7 +42,9 @@ exports.generateOwnerTier = (jobs_by_pk, isThreeTier, twotierpref) => {
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)} #${jobs_by_pk.owner.accountingid || ""}`
).trim();
)
.trim()
.replace(":", " ");
}
}
};

View File

@@ -523,6 +523,7 @@ function CalculateAdditional(job) {
additionalCostItems: [],
adjustments: null,
towing: null,
shipping: Dinero(),
storage: null,
pvrt: null,
total: null,
@@ -530,6 +531,7 @@ function CalculateAdditional(job) {
ret.towing = Dinero({
amount: Math.round((job.towing_payable || 0) * 100),
});
ret.additionalCosts = job.joblines
.filter((jl) => !jl.removed && IsAdditionalCost(jl))
.reduce((acc, val) => {
@@ -537,6 +539,11 @@ function CalculateAdditional(job) {
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1);
if (val.db_ref === "936004") {
//Shipping line IO-1921.
ret.shipping = ret.shipping.add(lineValue);
}
if (val.line_desc.toLowerCase().includes("towing")) {
ret.towing = lineValue;
return acc;