Compare commits

...

24 Commits

Author SHA1 Message Date
Patrick Fic
674c06665c IO-1911 Timetickets Scoreboard. 2022-06-06 09:54:02 -07:00
Patrick Fic
3feb1a3887 IO-1914 Add split tracking for inventory. 2022-06-06 09:53:42 -07:00
Patrick Fic
1258843f3d IO-1913 Add Rental Reservation key 2022-06-02 10:09:01 -07:00
Patrick Fic
485b5d0866 IO-1914 Update inventory insert queries. 2022-06-01 16:32:24 -07:00
Patrick Fic
62b1da0b64 IO-1914 Add bill refetch after insert. 2022-06-01 13:45:42 -07:00
Patrick Fic
a855853230 IO-1914 WIP Inventory. 2022-06-01 09:52:47 -07:00
Patrick Fic
d28d4d6283 IO-1914 Added inventory page. 2022-05-31 12:38:07 -07:00
Patrick Fic
912756e0f9 Missed in previous commit. 2022-05-30 17:48:29 -07:00
Patrick Fic
908c17aa68 IO-1914 Schema for inventory and basic adding to inventory. 2022-05-30 17:39:43 -07:00
Patrick Fic
96995fdd1b Add A/R Account to Receivables Export. 2022-05-30 16:30:49 -07:00
Patrick Fic
5341d93e29 Merged in release/2022-05-27 (pull request #495)
Remove console log statmenets.

Approved-by: Patrick Fic
2022-05-27 21:46:44 +00:00
Patrick Fic
b8d2dbc2e1 Remove console log statmenets. 2022-05-27 14:38:45 -07:00
Patrick Fic
595ec72edd IO-1912 Resolve date for tech clock in. 2022-05-27 14:34:02 -07:00
Patrick Fic
885a861f1e IO-1910 Include totals of scoreboard components. 2022-05-27 10:39:16 -07:00
Patrick Fic
b213e5d54f Fix link on active jobs page. 2022-05-26 15:19:46 -07:00
Patrick Fic
653692b2a5 IO-1868 Prevent returns on invoiced RO. 2022-05-26 15:11:44 -07:00
Patrick Fic
e1e5dda710 IO-1891 Add owner Note. 2022-05-26 12:09:36 -07:00
Patrick Fic
7f3b1413d7 Remove files. 2022-05-26 12:04:56 -07:00
Patrick Fic
528c68695f IO-1885 Resolve UTC time for parts orders. 2022-05-26 11:31:09 -07:00
Patrick Fic
65a18acdc1 Added loading to parts receive modal. 2022-05-25 08:37:14 -07:00
Patrick Fic
930b2791f2 IO-1907 Prevent NaN PVRT. 2022-05-24 16:59:25 -07:00
Patrick Fic
1cf5a1fba8 IO-1864 Mark bills as exported in bulk. 2022-05-24 16:44:10 -07:00
Patrick Fic
83137b2d96 Merged in release/2022-05-20 (pull request #490)
Release/2022 05 20
2022-05-20 17:33:07 +00:00
Patrick Fic
7e969e32b2 Merged in hotfix/2022-05-18 (pull request #484)
hotfix/2022-05-18

Approved-by: Patrick Fic
2022-05-18 19:00:12 +00:00
85 changed files with 2734 additions and 79420 deletions

View File

@@ -17213,6 +17213,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 +30169,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>
@@ -32076,6 +32332,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>note</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>ownr_addr1</name>
<definition_loaded>false</definition_loaded>
@@ -36237,6 +36514,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>rental_reservation</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>ro_totals</name>
<definition_loaded>false</definition_loaded>
@@ -40529,6 +40827,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>
@@ -40550,6 +40911,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>
@@ -40571,6 +40974,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>
@@ -40592,6 +41058,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>
@@ -42341,6 +42828,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>
@@ -43162,6 +43670,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

@@ -14,6 +14,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -28,7 +29,12 @@ export default connect(
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
export function AccountingPayablesTableComponent({
bodyshop,
loading,
bills,
refetch,
}) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -143,15 +149,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<div>
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
</div>
<PayableExportButton
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
),
},
];
@@ -177,6 +181,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
<Card
extra={
<Space wrap>
<BillMarkSelectedExported
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
<PayableExportAll
billids={selectedBills}
disabled={transInProgress || selectedBills.length === 0}

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

@@ -4,6 +4,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -14,7 +15,7 @@ import BillDeleteButton from "../bill-delete-button/bill-delete-button.component
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly,
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsListTableComponent({
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
@@ -58,7 +60,9 @@ export function BillsListTableComponent({
<BillDeleteButton bill={record} />
<Button
disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||
jobRO
}
onClick={() => {
setPartsOrderContext({

View File

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

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

@@ -137,9 +137,9 @@ export function JobsList({ bodyshop }) {
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.owner.id}
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />

View File

@@ -14,7 +14,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
return (
<div>
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("owners.forms.name")}>
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
<Input />
@@ -29,7 +28,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}>
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
<Input />
@@ -50,7 +48,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.contact")}>
<Form.Item
label={t("owners.fields.allow_text_message")}
@@ -98,6 +95,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
</LayoutFormRow>
<Form.Item label={t("owners.fields.note")} name="note">
<Input.TextArea rows={4} />
</Form.Item>
</div>
);
}

View File

@@ -59,6 +59,14 @@ export default function OwnerFindModalComponent({
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
),
},
{
title: t("owners.fields.note"),
dataIndex: "note",
key: "note",
render: (text, record) => (
<span style={{ whiteSpace: "pre" }}>{record.note}</span>
),
},
];
const handleOnRowClick = (record) => {

View File

@@ -101,6 +101,7 @@ export function PartsOrderModalContainer({
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -31,7 +31,7 @@ export function PartsReceiveModalContainer({
bodyshop,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { visible, context, actions } = partsReceiveModal;
const { partsorderlines } = context;
@@ -42,7 +42,7 @@ export function PartsReceiveModalContainer({
const handleFinish = async (values) => {
logImEXEvent("parts_order_receive");
setLoading(true);
const result = await Promise.all(
values.partsorderlines.map((li) => {
return receivePartsLine({
@@ -75,7 +75,7 @@ export function PartsReceiveModalContainer({
notification["success"]({
message: t("parts_orders.successes.received"),
});
setLoading(false);
if (refetch) refetch();
toggleModalVisible();
};
@@ -96,6 +96,7 @@ export function PartsReceiveModalContainer({
title={t("parts_orders.labels.receive")}
onCancel={() => toggleModalVisible()}
onOk={() => form.submit()}
okButtonProps={{ loading: loading }}
destroyOnClose
forceRender
width="50%"

View File

@@ -0,0 +1,79 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillMarkSelectedExported);
export function BillMarkSelectedExported({
billids,
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billIds: [uuid!]!) {
update_bills(where: { id: { _in: $billIds } }, _set: { exported: true }) {
returning {
id
exported
exported_at
}
}
}
`);
const handleUpdate = async () => {
setLoading(true);
loadingCallback(true);
const result = await updateBill({
variables: { billIds: billids },
update(cache){
}
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.markexported"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
loadingCallback(false);
completedCallback && completedCallback([]);
setLoading(false);
refetch && refetch();
};
return (
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
);
}

View File

@@ -42,17 +42,24 @@ export function ProductionColumnsComponent({
};
const columnKeys = columns.map((i) => i.key);
const cols = dataSource({
technician,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
});
const menu = (
<Menu onClick={handleAdd}>
{dataSource({
technician,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
})
<Menu
onClick={handleAdd}
style={{
columnCount: Math.max(Math.floor(cols.length / 10), 1),
}}
>
{cols
.filter((i) => !columnKeys.includes(i.key))
.map((item) => (
<Menu.Item key={item.key}>{item.title}</Menu.Item>
<Menu.Item key={item.key} style={{ breakInside: "avoid" }}>
{item.title}
</Menu.Item>
))}
</Menu>
);

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

@@ -177,6 +177,27 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Statistic value={values.toDatePaint.toFixed(1)} />
</Col>
</Row>
<Row>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}></Col>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
/>
</Col>
</Row>
</Col>
</Row>
</Card>

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,101 @@
import { Card, Col, Row, Space, Statistic, Table } 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={6}>
<Space wrap>
<Statistic
title={t("scoreboard.labels.lastweek")}
value={data.totalLastWeek.toFixed(1)}
/>
<Statistic
title={t("scoreboard.labels.thisweek")}
value={data.totalThisWeek.toFixed(1)}
/>
<Statistic
title={t("scoreboard.labels.lastmonth")}
value={data.totalLastMonth.toFixed(1)}
/>
<Statistic
title={t("scoreboard.labels.thismonth")}
value={data.totalThisMonth.toFixed(1)}
/>
</Space>
</Col>
<Col md={24} lg={18}>
<Table
columns={columns}
dataSource={tableData}
id="employee_number"
/>
</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

@@ -10,6 +10,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
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";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician,
@@ -27,14 +28,15 @@ export function TechClockInContainer({ technician, bodyshop }) {
const handleFinish = async (values) => {
setLoading(true);
const theTime = (await axios.post("/utils/time")).data;
const result = await insertTimeTicket({
variables: {
timeTicketInput: [
{
bodyshopid: bodyshop.id,
employeeid: technician.id,
date: theTime,
clockon: theTime,
date: moment(theTime).format("YYYY-MM-DD"),
clockon: moment(theTime),
jobid: values.jobid,
cost_center: values.cost_center,
ciecacode:

View File

@@ -55,9 +55,10 @@ export function TechClockOffButton({
timeticket: {
clockoff: (await axios.post("/utils/time")).data,
...values,
rate: emps && emps.rates.filter(
(r) => r.cost_center === values.cost_center
)[0]?.rate,
rate:
emps &&
emps.rates.filter((r) => r.cost_center === values.cost_center)[0]
?.rate,
ciecacode:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? values.cost_center

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

@@ -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

@@ -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

@@ -12,6 +12,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
ownr_ph1
ownr_ph2
ownr_ea
ownerid
comment
plate_no
plate_st

View File

@@ -15,6 +15,7 @@ export const QUERY_SEARCH_OWNER_BY_IDX = gql`
ownr_st
ownr_zip
id
note
}
}
`;
@@ -65,6 +66,7 @@ export const QUERY_OWNER_BY_ID = gql`
ownr_title
ownr_zip
preferred_contact
note
jobs {
id
ro_number

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,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

@@ -1069,6 +1069,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 +1774,7 @@
"export-logs": "Export Logs",
"help": "Help",
"home": "Home",
"inventory": "Inventory",
"jobs": "Jobs",
"newjob": "Create New Job",
"owners": "Owners",
@@ -1893,6 +1914,7 @@
"address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name",
"note": "Owner Note",
"ownr_addr1": "Address",
"ownr_addr2": "Address 2",
"ownr_city": "City",
@@ -2144,6 +2166,7 @@
"purchases_by_ro_detail": "Purchases - Detail",
"purchases_by_ro_summary": "Purchases - Summary",
"qc_sheet": "Quality Control Sheet",
"rental_reservation": "Rental Reservation",
"ro_totals": "RO Totals",
"ro_with_description": "RO Summary with Descriptions",
"sgi_certificate_of_repairs": "SGI - Certificate of Repairs",
@@ -2403,9 +2426,18 @@
"asoftodaytarget": "As of Today",
"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 Period",
"weeklyactual": "Actual (W)",
"weeklytarget": "Weekly",
"workingdays": "Working Days / Month"
@@ -2521,6 +2553,7 @@
"dashboard": "Dashboard",
"dms": "DMS Export",
"export-logs": "Export Logs",
"inventory": "Inventory",
"jobs": "Jobs",
"jobs-active": "Active Jobs",
"jobs-admin": "Admin",
@@ -2561,6 +2594,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

@@ -1069,6 +1069,26 @@
"printpack": ""
}
},
"inventory": {
"actions": {
"addtoinventory": "",
"consumefrominventory": ""
},
"errors": {
"inserting": ""
},
"labels": {
"consumedbyjob": "",
"frombillinvoicenumber": "",
"fromvendor": "",
"inventory": "",
"showall": "",
"showavailable": ""
},
"successes": {
"inserted": ""
}
},
"joblines": {
"actions": {
"new": ""
@@ -1754,6 +1774,7 @@
"export-logs": "",
"help": "",
"home": "Casa",
"inventory": "",
"jobs": "Trabajos",
"newjob": "",
"owners": "propietarios",
@@ -1893,6 +1914,7 @@
"address": "Dirección",
"allow_text_message": "Permiso de texto?",
"name": "Nombre",
"note": "",
"ownr_addr1": "Dirección",
"ownr_addr2": "Dirección 2",
"ownr_city": "ciudad",
@@ -2144,6 +2166,7 @@
"purchases_by_ro_detail": "",
"purchases_by_ro_summary": "",
"qc_sheet": "",
"rental_reservation": "",
"ro_totals": "",
"ro_with_description": "",
"sgi_certificate_of_repairs": "",
@@ -2403,9 +2426,18 @@
"asoftodaytarget": "",
"dailyactual": "",
"dailytarget": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
"monthlytarget": "",
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"todateactual": "",
"totaloverperiod": "",
"weeklyactual": "",
"weeklytarget": "",
"workingdays": ""
@@ -2521,6 +2553,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "",
"jobs-active": "",
"jobs-admin": "",
@@ -2561,6 +2594,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "",
"jobs-all": "",

View File

@@ -1069,6 +1069,26 @@
"printpack": ""
}
},
"inventory": {
"actions": {
"addtoinventory": "",
"consumefrominventory": ""
},
"errors": {
"inserting": ""
},
"labels": {
"consumedbyjob": "",
"frombillinvoicenumber": "",
"fromvendor": "",
"inventory": "",
"showall": "",
"showavailable": ""
},
"successes": {
"inserted": ""
}
},
"joblines": {
"actions": {
"new": ""
@@ -1754,6 +1774,7 @@
"export-logs": "",
"help": "",
"home": "Accueil",
"inventory": "",
"jobs": "Emplois",
"newjob": "",
"owners": "Propriétaires",
@@ -1893,6 +1914,7 @@
"address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom",
"note": "",
"ownr_addr1": "Adresse",
"ownr_addr2": "Adresse 2 ",
"ownr_city": "Ville",
@@ -2144,6 +2166,7 @@
"purchases_by_ro_detail": "",
"purchases_by_ro_summary": "",
"qc_sheet": "",
"rental_reservation": "",
"ro_totals": "",
"ro_with_description": "",
"sgi_certificate_of_repairs": "",
@@ -2403,9 +2426,18 @@
"asoftodaytarget": "",
"dailyactual": "",
"dailytarget": "",
"jobs": "",
"lastmonth": "",
"lastweek": "",
"monthlytarget": "",
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"targets": "",
"thismonth": "",
"thisweek": "",
"timetickets": "",
"todateactual": "",
"totaloverperiod": "",
"weeklyactual": "",
"weeklytarget": "",
"workingdays": ""
@@ -2521,6 +2553,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "",
"jobs-active": "",
"jobs-admin": "",
@@ -2561,6 +2594,7 @@
"dashboard": "",
"dms": "",
"export-logs": "",
"inventory": "",
"jobs": "Tous les emplois | $t(titles.app)",
"jobs-admin": "",
"jobs-all": "",

View File

@@ -480,6 +480,14 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "ro",
},
rental_reservation: {
title: i18n.t("printcenter.jobs.rental_reservation"),
description: "CASL Authorization",
subject: i18n.t("printcenter.jobs.rental_reservation"),
key: "rental_reservation",
disabled: false,
group: "pre",
},
}
: {}),
...(!type || type === "job_special"

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:
@@ -3823,32 +3943,36 @@
- active:
_eq: true
columns:
- id
- accountingid
- allow_text_message
- created_at
- updated_at
- ownr_fn
- ownr_ln
- id
- note
- ownr_addr1
- ownr_addr2
- ownr_city
- ownr_st
- ownr_zip
- ownr_co_nm
- ownr_ctry
- ownr_ea
- ownr_fn
- ownr_ln
- ownr_ph1
- preferred_contact
- allow_text_message
- shopid
- ownr_ph2
- ownr_co_nm
- ownr_st
- ownr_title
- accountingid
- ownr_zip
- preferred_contact
- shopid
- updated_at
select_permissions:
- role: user
permission:
columns:
- allow_text_message
- accountingid
- allow_text_message
- created_at
- id
- note
- ownr_addr1
- ownr_addr2
- ownr_city
@@ -3863,10 +3987,8 @@
- ownr_title
- ownr_zip
- preferred_contact
- created_at
- updated_at
- id
- shopid
- updated_at
filter:
bodyshop:
associations:
@@ -3881,8 +4003,11 @@
- role: user
permission:
columns:
- allow_text_message
- accountingid
- allow_text_message
- created_at
- id
- note
- ownr_addr1
- ownr_addr2
- ownr_city
@@ -3897,10 +4022,8 @@
- ownr_title
- ownr_zip
- preferred_contact
- created_at
- updated_at
- id
- shopid
- updated_at
filter:
bodyshop:
associations:

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"."owners" add column "note" text
-- null;

View File

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

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);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -249,7 +249,9 @@ 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 } }
: {}),

View File

@@ -800,7 +800,7 @@ const CreateCosts = (job) => {
};
const StatusMapping = (status, md_ro_statuses) => {
//EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
//Possible return statuses EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
const {
default_imported,
default_open,
@@ -823,8 +823,6 @@ const StatusMapping = (status, md_ro_statuses) => {
else if (status === default_void) return "VOID";
else if (md_ro_statuses.production_statuses.includes(status)) return "IPR";
else return "UNDEFINED";
// default: return "UNDEFINED"
};
const GenerateDetailLines = (job, line, statuses) => {

View File

@@ -37,7 +37,7 @@ exports.totalsSsu = async function (req, res) {
clm_total: newTotals.totals.total_repairs.toFormat("0.00"),
owner_owing: newTotals.totals.custPayable.total.toFormat("0.00"),
job_totals: newTotals,
queued_for_parts: true,
//queued_for_parts: true,
},
});