From 96995fdd1bca5037a44d457233bb4f772aab67fa Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 30 May 2022 16:30:49 -0700 Subject: [PATCH 01/18] Add A/R Account to Receivables Export. --- server/accounting/qbxml/qbxml-receivables.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 0641c67c3..78c7389f7 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -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 } } : {}), From 908c17aa6870c4bbc747e66c10b508b5e3ebe486 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 30 May 2022 17:39:43 -0700 Subject: [PATCH 02/18] IO-1914 Schema for inventory and basic adding to inventory. --- bodyshop_translations.babel | 130 +++++++++++++++ .../bill-detail-edit.container.jsx | 4 +- .../bill-enter-modal.container.jsx | 31 +++- .../bill-form/bill-form.component.jsx | 12 +- .../bill-form/bill-form.container.jsx | 20 +++ .../bill-form/bill-form.lines.component.jsx | 21 ++- .../bill-form/bill-form.totals.utility.js | 1 + .../bill-inventory-table.component.jsx | 153 ++++++++++++++++++ .../bill-inventory-table.styles.scss | 19 +++ .../billline-add-inventory.component.jsx | 103 ++++++++++++ client/src/graphql/inventory.queries.js | 49 ++++++ client/src/translations/en_us/common.json | 15 ++ client/src/translations/es/common.json | 15 ++ client/src/translations/fr/common.json | 15 ++ hasura/metadata/tables.yaml | 119 ++++++++++++++ .../down.sql | 1 + .../up.sql | 18 +++ .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 5 + .../up.sql | 5 + .../down.sql | 5 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 2 + .../down.sql | 1 + .../up.sql | 2 + 35 files changed, 769 insertions(+), 9 deletions(-) create mode 100644 client/src/components/bill-inventory-table/bill-inventory-table.component.jsx create mode 100644 client/src/components/bill-inventory-table/bill-inventory-table.styles.scss create mode 100644 client/src/components/billline-add-inventory/billline-add-inventory.component.jsx create mode 100644 client/src/graphql/inventory.queries.js create mode 100644 hasura/migrations/1653933012524_create_table_public_inventory/down.sql create mode 100644 hasura/migrations/1653933012524_create_table_public_inventory/up.sql create mode 100644 hasura/migrations/1653933089451_set_fk_public_inventory_billid/down.sql create mode 100644 hasura/migrations/1653933089451_set_fk_public_inventory_billid/up.sql create mode 100644 hasura/migrations/1653933105635_set_fk_public_inventory_shopid/down.sql create mode 100644 hasura/migrations/1653933105635_set_fk_public_inventory_shopid/up.sql create mode 100644 hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/down.sql create mode 100644 hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/up.sql create mode 100644 hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/down.sql create mode 100644 hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/up.sql create mode 100644 hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/down.sql create mode 100644 hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/up.sql create mode 100644 hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/down.sql create mode 100644 hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/up.sql create mode 100644 hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/down.sql create mode 100644 hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/up.sql create mode 100644 hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/down.sql create mode 100644 hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/up.sql create mode 100644 hasura/migrations/1653953399350_create_index_inventory_shopididx/down.sql create mode 100644 hasura/migrations/1653953399350_create_index_inventory_shopididx/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 5f5bb62e5..ec94be6a6 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -17213,6 +17213,136 @@ + + inventory + + + actions + + + addtoinventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + consumefrominventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + errors + + + inserting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + labels + + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + successes + + + inserted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + joblines diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx index 9bbca0a60..f2f03e90d 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx @@ -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]] diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index bfadbcd37..82948fe88 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -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 diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index f57c56819..7d7eb793a 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -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) { + 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} /> )} diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx index 4fe056667..836a24be4 100644 --- a/client/src/components/bill-form/bill-form.container.jsx +++ b/client/src/components/bill-form/bill-form.container.jsx @@ -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 && ( )} + {!billEdit && Simple_Inventory.treatment === "on" && ( + + )} ); } diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index b8a1b5c9d..b3d6209c5 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -8,7 +8,7 @@ import { Space, Switch, Table, - Tooltip + Tooltip, } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -18,6 +18,7 @@ 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"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -34,6 +35,7 @@ export function BillEnterModalLinesComponent({ discount, form, responsibilityCenters, + billEdit, }) { const { t } = useTranslation(); const { setFieldsValue, getFieldsValue, getFieldValue } = form; @@ -477,9 +479,20 @@ export function BillEnterModalLinesComponent({ dataIndex: "actions", render: (text, record) => ( - + + + + {() => ( + + )} + + ), }, ]; diff --git a/client/src/components/bill-form/bill-form.totals.utility.js b/client/src/components/bill-form/bill-form.totals.utility.js index df84ec86e..05396e66c 100644 --- a/client/src/components/bill-form/bill-form.totals.utility.js +++ b/client/src/components/bill-form/bill-form.totals.utility.js @@ -18,6 +18,7 @@ export const CalculateBillTotal = (invoice) => { amount: Math.round((i.actual_cost || 0) * 100), }).multiply(i.quantity || 1); + console.log(i, itemTotal.toFormat); subtotal = subtotal.add(itemTotal); if (i.applicable_taxes.federal) { federalTax = federalTax.add( diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx new file mode 100644 index 000000000..d8fb121d0 --- /dev/null +++ b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx @@ -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 ( + prev.vendorid !== cur.vendorid} + noStyle + > + {() => { + const is_inhouse = + form.getFieldValue("vendorid") === bodyshop.inhousevendorid; + + if (!is_inhouse || billEdit) { + return null; + } + + if (inventoryLoading) return ; + + return ( + + {(fields, { add, remove, move }) => { + return ( + <> + + {t("inventory.labels.inventory")} + + + + + + + + + + + + + + {fields.map((field, index) => ( + + + + + + + + + + + ))} + +
{t("billlines.fields.line_desc")}{t("vendors.fields.name")}{t("billlines.fields.quantity")}{t("billlines.fields.actual_price")}{t("billlines.fields.actual_cost")}{t("inventory.actions.consumefrominventory")}
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ); + }} +
+ ); + }} +
+ ); +} diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss new file mode 100644 index 000000000..67bc0543b --- /dev/null +++ b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss @@ -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; + } +} \ No newline at end of file diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx new file mode 100644 index 000000000..c6e9f63f4 --- /dev/null +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -0,0 +1,103 @@ +import { FileAddFilled } from "@ant-design/icons"; +import { useApolloClient, useMutation } from "@apollo/client"; +import { Button, notification, Tooltip } from "antd"; +import { json } from "body-parser"; +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 } from "../../redux/user/user.selectors"; +import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(BilllineAddInventory); + +export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { + const [loading, setLoading] = useState(false); + + 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: billline.applicable_taxes.local, + state: billline.applicable_taxes.state, + federal: billline.applicable_taxes.federal, + }, + }, + ], + }; + + cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100; + + const insertResult = await insertInventoryLine({ + variables: { + 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. + }, + }); + + 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 ( + + + + ); +} diff --git a/client/src/graphql/inventory.queries.js b/client/src/graphql/inventory.queries.js new file mode 100644 index 000000000..8c5bb7a4b --- /dev/null +++ b/client/src/graphql/inventory.queries.js @@ -0,0 +1,49 @@ +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! + ) { + insert_inventory_one(object: $inv) { + id + } + insert_bills_one(object: $cm) { + id + } + } +`; +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 + } + } + } + } + } +`; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c684228ce..e34a885b6 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1069,6 +1069,21 @@ "printpack": "Intake Print Pack" } }, + "inventory": { + "actions": { + "addtoinventory": "Add to Inventory", + "consumefrominventory": "Consume from Inventory?" + }, + "errors": { + "inserting": "Error inserting inventory item. {{error}}" + }, + "labels": { + "inventory": "Inventory" + }, + "successes": { + "inserted": "Added line to inventory." + } + }, "joblines": { "actions": { "new": "New Line" diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index af04f5bb1..c7bc888ca 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1069,6 +1069,21 @@ "printpack": "" } }, + "inventory": { + "actions": { + "addtoinventory": "", + "consumefrominventory": "" + }, + "errors": { + "inserting": "" + }, + "labels": { + "inventory": "" + }, + "successes": { + "inserted": "" + } + }, "joblines": { "actions": { "new": "" diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index fbd28bd87..bc00188b9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1069,6 +1069,21 @@ "printpack": "" } }, + "inventory": { + "actions": { + "addtoinventory": "", + "consumefrominventory": "" + }, + "errors": { + "inserting": "" + }, + "labels": { + "inventory": "" + }, + "successes": { + "inserted": "" + } + }, "joblines": { "actions": { "new": "" diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 18224906e..b2d9e60d1 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -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,96 @@ - 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 + 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 +2323,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: diff --git a/hasura/migrations/1653933012524_create_table_public_inventory/down.sql b/hasura/migrations/1653933012524_create_table_public_inventory/down.sql new file mode 100644 index 000000000..8fac4e21a --- /dev/null +++ b/hasura/migrations/1653933012524_create_table_public_inventory/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."inventory"; diff --git a/hasura/migrations/1653933012524_create_table_public_inventory/up.sql b/hasura/migrations/1653933012524_create_table_public_inventory/up.sql new file mode 100644 index 000000000..32e80acb1 --- /dev/null +++ b/hasura/migrations/1653933012524_create_table_public_inventory/up.sql @@ -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; diff --git a/hasura/migrations/1653933089451_set_fk_public_inventory_billid/down.sql b/hasura/migrations/1653933089451_set_fk_public_inventory_billid/down.sql new file mode 100644 index 000000000..10359e17b --- /dev/null +++ b/hasura/migrations/1653933089451_set_fk_public_inventory_billid/down.sql @@ -0,0 +1 @@ +alter table "public"."inventory" drop constraint "inventory_billid_fkey"; diff --git a/hasura/migrations/1653933089451_set_fk_public_inventory_billid/up.sql b/hasura/migrations/1653933089451_set_fk_public_inventory_billid/up.sql new file mode 100644 index 000000000..083b9da5f --- /dev/null +++ b/hasura/migrations/1653933089451_set_fk_public_inventory_billid/up.sql @@ -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; diff --git a/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/down.sql b/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/down.sql new file mode 100644 index 000000000..2fb006d66 --- /dev/null +++ b/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/down.sql @@ -0,0 +1 @@ +alter table "public"."inventory" drop constraint "inventory_shopid_fkey"; diff --git a/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/up.sql b/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/up.sql new file mode 100644 index 000000000..b577f28bf --- /dev/null +++ b/hasura/migrations/1653933105635_set_fk_public_inventory_shopid/up.sql @@ -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; diff --git a/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/down.sql b/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/down.sql new file mode 100644 index 000000000..9ec9e56ea --- /dev/null +++ b/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/down.sql @@ -0,0 +1 @@ +alter table "public"."inventory" rename column "billlineid" to "billid"; diff --git a/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/up.sql b/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/up.sql new file mode 100644 index 000000000..aaf49eceb --- /dev/null +++ b/hasura/migrations/1653935727610_alter_table_public_inventory_alter_column_billid/up.sql @@ -0,0 +1 @@ +alter table "public"."inventory" rename column "billid" to "billlineid"; diff --git a/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/down.sql b/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/down.sql new file mode 100644 index 000000000..0e361bc38 --- /dev/null +++ b/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/down.sql @@ -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; diff --git a/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/up.sql b/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/up.sql new file mode 100644 index 000000000..8b21a892a --- /dev/null +++ b/hasura/migrations/1653953166273_alter_table_public_inventory_add_column_consumedbybillid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."inventory" add column "consumedbybillid" uuid + null; diff --git a/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/down.sql b/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/down.sql new file mode 100644 index 000000000..5d24c2671 --- /dev/null +++ b/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/down.sql @@ -0,0 +1 @@ +alter table "public"."inventory" drop constraint "inventory_consumedbybillid_fkey"; diff --git a/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/up.sql b/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/up.sql new file mode 100644 index 000000000..76c4bd767 --- /dev/null +++ b/hasura/migrations/1653953259127_set_fk_public_inventory_consumedbybillid/up.sql @@ -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; diff --git a/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/down.sql b/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/down.sql new file mode 100644 index 000000000..2e6cdae18 --- /dev/null +++ b/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/down.sql @@ -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; diff --git a/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/up.sql b/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/up.sql new file mode 100644 index 000000000..ee8578516 --- /dev/null +++ b/hasura/migrations/1653953283374_set_fk_public_inventory_consumedbybillid/up.sql @@ -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; diff --git a/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/down.sql b/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/down.sql new file mode 100644 index 000000000..2e6cdae18 --- /dev/null +++ b/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/down.sql @@ -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; diff --git a/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/up.sql b/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/up.sql new file mode 100644 index 000000000..ac2008fc6 --- /dev/null +++ b/hasura/migrations/1653953288756_set_fk_public_inventory_consumedbybillid/up.sql @@ -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; diff --git a/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/down.sql b/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/down.sql new file mode 100644 index 000000000..d2d1f2b46 --- /dev/null +++ b/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."inventory_consumedbybillid"; diff --git a/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/up.sql b/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/up.sql new file mode 100644 index 000000000..f547132c9 --- /dev/null +++ b/hasura/migrations/1653953385835_create_index_inventory_consumedbybillid/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "inventory_consumedbybillid" on + "public"."inventory" using btree ("consumedbybillid"); diff --git a/hasura/migrations/1653953399350_create_index_inventory_shopididx/down.sql b/hasura/migrations/1653953399350_create_index_inventory_shopididx/down.sql new file mode 100644 index 000000000..1b7d82158 --- /dev/null +++ b/hasura/migrations/1653953399350_create_index_inventory_shopididx/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."inventory_shopididx"; diff --git a/hasura/migrations/1653953399350_create_index_inventory_shopididx/up.sql b/hasura/migrations/1653953399350_create_index_inventory_shopididx/up.sql new file mode 100644 index 000000000..f987787cc --- /dev/null +++ b/hasura/migrations/1653953399350_create_index_inventory_shopididx/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "inventory_shopididx" on + "public"."inventory" using btree ("shopid"); From 912756e0f9ed3175375d82e874135ce4328aa83d Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 30 May 2022 17:48:29 -0700 Subject: [PATCH 03/18] Missed in previous commit. --- .../billline-add-inventory.component.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx index c6e9f63f4..f25ee2c61 100644 --- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -1,7 +1,6 @@ import { FileAddFilled } from "@ant-design/icons"; -import { useApolloClient, useMutation } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import { Button, notification, Tooltip } from "antd"; -import { json } from "body-parser"; import { t } from "i18next"; import moment from "moment"; import React, { useState } from "react"; From d28d4d62834fb2a1115c4289e2ffe217e268ae8f Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 31 May 2022 12:38:07 -0700 Subject: [PATCH 04/18] IO-1914 Added inventory page. --- bodyshop_translations.babel | 168 ++++++++++++++++++ .../bill-form/bill-form.totals.utility.js | 1 - .../components/header/header.component.jsx | 9 +- .../inventory-list.component.jsx | 156 ++++++++++++++++ .../inventory-list.container.jsx | 67 +++++++ .../components/rbac-wrapper/rbac-defaults.js | 4 +- .../shop-info/shop-info.rbac.component.jsx | 12 ++ client/src/graphql/inventory.queries.js | 50 ++++++ client/src/pages/inventory/inventory.page.jsx | 32 ++++ .../pages/manage/manage.page.component.jsx | 6 + client/src/translations/en_us/common.json | 10 +- client/src/translations/es/common.json | 10 +- client/src/translations/fr/common.json | 10 +- hasura/metadata/functions.yaml | 3 + hasura/metadata/tables.yaml | 1 + .../1654022042838_run_sql_migration/down.sql | 40 +++++ .../1654022042838_run_sql_migration/up.sql | 38 ++++ .../1654023497348_run_sql_migration/down.sql | 37 ++++ .../1654023497348_run_sql_migration/up.sql | 35 ++++ .../1654023506265_run_sql_migration/down.sql | 37 ++++ .../1654023506265_run_sql_migration/up.sql | 35 ++++ .../1654023641223_run_sql_migration/down.sql | 37 ++++ .../1654023641223_run_sql_migration/up.sql | 35 ++++ .../1654023762220_run_sql_migration/down.sql | 3 + .../1654023762220_run_sql_migration/up.sql | 1 + 25 files changed, 830 insertions(+), 7 deletions(-) create mode 100644 client/src/components/inventory-list/inventory-list.component.jsx create mode 100644 client/src/components/inventory-list/inventory-list.container.jsx create mode 100644 client/src/pages/inventory/inventory.page.jsx create mode 100644 hasura/migrations/1654022042838_run_sql_migration/down.sql create mode 100644 hasura/migrations/1654022042838_run_sql_migration/up.sql create mode 100644 hasura/migrations/1654023497348_run_sql_migration/down.sql create mode 100644 hasura/migrations/1654023497348_run_sql_migration/up.sql create mode 100644 hasura/migrations/1654023506265_run_sql_migration/down.sql create mode 100644 hasura/migrations/1654023506265_run_sql_migration/up.sql create mode 100644 hasura/migrations/1654023641223_run_sql_migration/down.sql create mode 100644 hasura/migrations/1654023641223_run_sql_migration/up.sql create mode 100644 hasura/migrations/1654023762220_run_sql_migration/down.sql create mode 100644 hasura/migrations/1654023762220_run_sql_migration/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index ec94be6a6..eac8b6536 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -17292,6 +17292,69 @@ labels + + consumedbyjob + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + frombillinvoicenumber + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + fromvendor + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + inventory false @@ -17313,6 +17376,48 @@ + + showall + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + showavailable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -30064,6 +30169,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false @@ -42492,6 +42618,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false @@ -43313,6 +43460,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false diff --git a/client/src/components/bill-form/bill-form.totals.utility.js b/client/src/components/bill-form/bill-form.totals.utility.js index 05396e66c..df84ec86e 100644 --- a/client/src/components/bill-form/bill-form.totals.utility.js +++ b/client/src/components/bill-form/bill-form.totals.utility.js @@ -18,7 +18,6 @@ export const CalculateBillTotal = (invoice) => { amount: Math.round((i.actual_cost || 0) * 100), }).multiply(i.quantity || 1); - console.log(i, itemTotal.toFormat); subtotal = subtotal.add(itemTotal); if (i.applicable_taxes.federal) { federalTax = federalTax.add( diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 2a4e9698e..63af81751 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -200,6 +200,13 @@ function Header({ {t("menus.header.enterbills")} + } + > + {t("menus.header.inventory")} + + }> {t("menus.header.allpayments")} @@ -216,7 +223,6 @@ function Header({ {t("menus.header.enterpayment")} - }> {t("menus.header.timetickets")} @@ -235,7 +241,6 @@ function Header({ {t("menus.header.entertimeticket")} - ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function JobsList({ bodyshop, refetch, loading, jobs, total }) { + const search = queryString.parse(useLocation().search); + const { page, sortcolumn, sortorder, invfilters } = 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) => ( + {record.actual_price} + ), + }, + { + title: t("billlines.fields.actual_cost"), + dataIndex: "actual_cost", + key: "actual_cost", + + render: (text, record) => ( + {record.actual_cost} + ), + }, + { + 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 ( + + {search.search && ( + <> + + {t("general.labels.searchresults", { search: search.search })} + + + + )} + + + + + { + search.search = value; + history.push({ search: queryString.stringify(search) }); + }} + enterButton + /> + + } + > + + + ); +} +export default connect(mapStateToProps, mapDispatchToProps)(JobsList); diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx new file mode 100644 index 000000000..568b3162a --- /dev/null +++ b/client/src/components/inventory-list/inventory-list.container.jsx @@ -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 ; + return ( + + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(InventoryList); diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js index 089e8954e..2f83443ac 100644 --- a/client/src/components/rbac-wrapper/rbac-defaults.js +++ b/client/src/components/rbac-wrapper/rbac-defaults.js @@ -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; diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index 3c0f75c88..4c425ff69 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -633,6 +633,18 @@ export default function ShopInfoRbacComponent({ form }) { > + + + ); diff --git a/client/src/graphql/inventory.queries.js b/client/src/graphql/inventory.queries.js index 8c5bb7a4b..a2c36f6c1 100644 --- a/client/src/graphql/inventory.queries.js +++ b/client/src/graphql/inventory.queries.js @@ -47,3 +47,53 @@ export const QUERY_OUTSTANDING_INVENTORY = gql` } } `; + +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) + } + } + } +`; diff --git a/client/src/pages/inventory/inventory.page.jsx b/client/src/pages/inventory/inventory.page.jsx new file mode 100644 index 000000000..26cc2f2dc --- /dev/null +++ b/client/src/pages/inventory/inventory.page.jsx @@ -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 ( + + + + ); +} + +export default connect(null, mapDispatchToProps)(InventoryPage); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 0fc635598..82017ffae 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -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 }) { + Date: Wed, 1 Jun 2022 09:52:47 -0700 Subject: [PATCH 05/18] IO-1914 WIP Inventory. --- .../bill-form/bill-form.component.jsx | 2 +- .../bill-form/bill-form.container.jsx | 4 +-- .../billline-add-inventory.component.jsx | 6 ++++- .../components/header/header.component.jsx | 27 ++++++++++++++----- .../inventory-list.component.jsx | 7 ++--- client/src/graphql/bills.queries.js | 7 +++++ 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 7d7eb793a..a9eb07443 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -98,7 +98,7 @@ export function BillFormComponent({ } } - if (vendorId === bodyshop.inhousevendorid) { + if (vendorId === bodyshop.inhousevendorid && !billEdit) { loadInventory(); } }, [ diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx index 836a24be4..358bb7f38 100644 --- a/client/src/components/bill-form/bill-form.container.jsx +++ b/client/src/components/bill-form/bill-form.container.jsx @@ -68,11 +68,11 @@ export function BillFormContainer({ returnData={returnData} /> )} - {!billEdit && Simple_Inventory.treatment === "on" && ( + {Simple_Inventory.treatment === "on" && ( )} diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx index f25ee2c61..5b099eadf 100644 --- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -94,7 +94,11 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { return ( - diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 63af81751..85aa0c200 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -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,13 +206,19 @@ function Header({ > {t("menus.header.enterbills")} - - } - > - {t("menus.header.inventory")} - + {Simple_Inventory.treatment === "on" && ( + <> + + } + > + + {t("menus.header.inventory")} + + + + )} }> {t("menus.header.allpayments")} diff --git a/client/src/components/inventory-list/inventory-list.component.jsx b/client/src/components/inventory-list/inventory-list.component.jsx index 7ddb6595f..58210eb94 100644 --- a/client/src/components/inventory-list/inventory-list.component.jsx +++ b/client/src/components/inventory-list/inventory-list.component.jsx @@ -1,16 +1,13 @@ import { SyncOutlined } from "@ant-design/icons"; import { Button, Card, Input, Space, Table, Typography } from "antd"; -import _ from "lodash"; import queryString from "query-string"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import StartChatButton from "../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, @@ -21,7 +18,7 @@ const mapDispatchToProps = (dispatch) => ({ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { const search = queryString.parse(useLocation().search); - const { page, sortcolumn, sortorder, invfilters } = search; + const { page, sortcolumn, sortorder } = search; const history = useHistory(); const { t } = useTranslation(); diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js index dd6e95715..f042fd7ae 100644 --- a/client/src/graphql/bills.queries.js +++ b/client/src/graphql/bills.queries.js @@ -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 From 62b1da0b641f2ab329c6405d8b2cec92276472d0 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 1 Jun 2022 13:45:42 -0700 Subject: [PATCH 06/18] IO-1914 Add bill refetch after insert. --- .../billline-add-inventory/billline-add-inventory.component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx index 5b099eadf..d84bf4b0d 100644 --- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -73,6 +73,7 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { }, cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert. }, + refetchQueries: ["QUERY_BILL_BY_PK"], }); if (!insertResult.errors) { From 485b5d086663f118fc3e9d33a877cc2f7626e736 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 1 Jun 2022 16:32:24 -0700 Subject: [PATCH 07/18] IO-1914 Update inventory insert queries. --- .../bill-form/bill-form.lines.component.jsx | 1 + .../billline-add-inventory.component.jsx | 52 +++++- .../scoreboard-targets-table.util.js | 17 ++ .../scoreboard-timetickets.bar.component.jsx | 157 ++++++++++++++++++ .../scoreboard-timetickets.component.jsx | 45 +++++ client/src/graphql/inventory.queries.js | 13 ++ .../scoreboard/scoreboard.page.container.jsx | 31 +++- 7 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx create mode 100644 client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index b3d6209c5..5891741d7 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -36,6 +36,7 @@ export function BillEnterModalLinesComponent({ form, responsibilityCenters, billEdit, + billid, }) { const { t } = useTranslation(); const { setFieldsValue, getFieldsValue, getFieldValue } = form; diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx index d84bf4b0d..2a6d76355 100644 --- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -7,11 +7,17 @@ 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 } from "../../redux/user/user.selectors"; +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)) @@ -21,8 +27,15 @@ export default connect( mapDispatchToProps )(BilllineAddInventory); -export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { +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); @@ -51,9 +64,9 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { cost_center: billline.cost_center, deductedfromlbr: billline.deductedfromlbr, applicable_taxes: { - local: billline.applicable_taxes.local, - state: billline.applicable_taxes.state, - federal: billline.applicable_taxes.federal, + local: false, //billline.applicable_taxes.local, + state: false, //billline.applicable_taxes.state, + federal: false, // billline.applicable_taxes.federal, }, }, ], @@ -63,6 +76,8 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { const insertResult = await insertInventoryLine({ variables: { + joblineId: billline.joblineid, + joblineStatus: bodyshop.md_order_statuses.default_returned, inv: { shopid: bodyshop.id, billlineid: billline.id, @@ -72,6 +87,31 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { 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"], }); @@ -97,7 +137,7 @@ export function BilllineAddInventory({ bodyshop, billline, disabled, jobid }) { - {() => ( - - )} + {() => + Simple_Inventory.treatment === "on" && ( + + ) + } ), From 674c06665cfc250ec2b9aea3b331e44ec1045b8a Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 09:54:02 -0700 Subject: [PATCH 10/18] IO-1911 Timetickets Scoreboard. --- bodyshop_translations.babel | 189 ++++++++++++ .../scoreboard-targets-table.util.js | 5 - .../scoreboard-timetickets.bar.component.jsx | 128 ++------ .../scoreboard-timetickets.component.jsx | 288 +++++++++++++++++- ...scoreboard-timetickets.stats.component.jsx | 101 ++++++ .../time-tickets-dates-selector.component.jsx | 2 + client/src/graphql/jobs-lines.queries.js | 1 + client/src/graphql/timetickets.queries.js | 36 ++- .../scoreboard/scoreboard.page.container.jsx | 29 +- client/src/translations/en_us/common.json | 9 + client/src/translations/es/common.json | 9 + client/src/translations/fr/common.json | 9 + 12 files changed, 674 insertions(+), 132 deletions(-) create mode 100644 client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index a6b7d0456..22823dc7c 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -40827,6 +40827,69 @@ + + jobs + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lastmonth + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lastweek + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + monthlytarget false @@ -40848,6 +40911,48 @@ + + productivestatistics + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + productivetimeticketsoverdate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + targets false @@ -40869,6 +40974,69 @@ + + thismonth + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + thisweek + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + timetickets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + todateactual false @@ -40890,6 +41058,27 @@ + + totaloverperiod + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + weeklyactual false diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js index 2b9c0b3f7..9f925a1a3 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js @@ -49,11 +49,6 @@ export const ListOfDaysInCurrentMonth = () => { }; export const ListDaysBetween = ({ start, end }) => { - console.log( - "🚀 ~ file: scoreboard-targets-table.util.js ~ line 52 ~ start, end", - start, - end - ); const days = []; const dateStart = moment(start); const dateEnd = moment(end); diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx index 427766d3b..063638a47 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx @@ -1,14 +1,12 @@ import { Card } from "antd"; -import moment from "moment"; -import React, { useMemo } from "react"; +import React from "react"; +import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { - Area, Bar, CartesianGrid, ComposedChart, Legend, - Line, ResponsiveContainer, Tooltip, XAxis, @@ -16,9 +14,7 @@ import { } from "recharts"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util"; -import _ from "lodash"; - +import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component"; const graphProps = { strokeWidth: 3, }; @@ -35,86 +31,16 @@ export default connect( mapDispatchToProps )(ScoreboardTicketsBar); -export function ScoreboardTicketsBar({ start, end, timetickets, bodyshop }) { - console.log( - "🚀 ~ file: scoreboard-timetickets.bar.component.jsx ~ line 39 ~ start, end,", - start, - end - ); - const listOfBusDays = Utils.ListOfDaysInCurrentMonth(); - - const data = useMemo(() => { - const ticketsGroupedByDate = _.groupBy(timetickets, "date"); - - const listOfDays = Utils.ListDaysBetween({ start, end }); - console.log( - "🚀 ~ file: scoreboard-timetickets.bar.component.jsx ~ line 45 ~ listOfDays", - listOfDays - ); - - console.log( - "🚀 ~ file: scoreboard-timetickets.bar.component.jsx ~ line 43 ~ groupedByDate", - ticketsGroupedByDate - ); - - const ret = []; - listOfDays.forEach((day) => { - const r = { - date: day, - total: 0, - }; - }); - }, [timetickets]); - // const data = listOfBusDays.reduce((acc, val) => { - // //Sum up the current day. - - // const groupedbyDate = _.groupBy(data, "date"); - // let dayhrs; - // if (!!sbEntriesByDate[val]) { - // dayhrs = sbEntriesByDate[val].reduce( - // (dayAcc, dayVal) => { - // return { - // bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs, - // painthrs: dayAcc.painthrs + dayVal.painthrs, - // }; - // }, - // { bodyhrs: 0, painthrs: 0 } - // ); - // } else { - // dayhrs = { - // bodyhrs: 0, - // painthrs: 0, - // }; - // } - - // const theValue = { - // date: moment(val).format("D ddd"), - // paintHrs: _.round(dayhrs.painthrs, 1), - // bodyHrs: _.round(dayhrs.bodyhrs, 1), - // accTargetHrs: _.round( - // Utils.AsOfDateTargetHours( - // bodyshop.scoreboard_target.dailyBodyTarget + - // bodyshop.scoreboard_target.dailyPaintTarget, - // val - // ), - // 1 - // ), - // accHrs: _.round( - // acc.length > 0 - // ? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs - // : dayhrs.painthrs + dayhrs.bodyhrs, - // 1 - // ), - // }; - - // return [...acc, theValue]; - // }, []); - +export function ScoreboardTicketsBar({ data, bodyshop }) { + const { t } = useTranslation(); return ( - + } + > @@ -122,34 +48,32 @@ export function ScoreboardTicketsBar({ start, end, timetickets, bodyshop }) { - - - - */} + {data && + data.employees.map((e, idx) => ( + + ))} + + {/* + /> */} diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx index fae8f4379..f85982359 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -1,12 +1,16 @@ -import React from "react"; -import { useLocation } from "react-router-dom"; -import queryString from "query-string"; -import moment from "moment"; 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 TimeTicketsDatesSelector from "../../components/ticket-tickets-dates-selector/time-tickets-dates-selector.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); @@ -15,25 +19,230 @@ export default function ScoreboardTimeTickets() { ? 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, - end: endDate, + 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, }); - if (error) return ; + 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 ; + if (loading) return ; return ( -
- - -
+ +
+ + + + + + ); } @@ -43,3 +252,50 @@ export default function ScoreboardTimeTickets() { //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; +} diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx new file mode 100644 index 000000000..863f5f14f --- /dev/null +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -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 ( + + + + + + + + + + + +
+ + + + ); +} diff --git a/client/src/components/ticket-tickets-dates-selector/time-tickets-dates-selector.component.jsx b/client/src/components/ticket-tickets-dates-selector/time-tickets-dates-selector.component.jsx index b7a5ac44b..a827e3fd7 100644 --- a/client/src/components/ticket-tickets-dates-selector/time-tickets-dates-selector.component.jsx +++ b/client/src/components/ticket-tickets-dates-selector/time-tickets-dates-selector.component.jsx @@ -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, }), diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index 3617df239..b2184f072 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -61,6 +61,7 @@ export const GET_LINE_TICKET_BY_PK = gql` flat_rate clockon clockoff + rate employee { id first_name diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index 057edc1cb..12d370560 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -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 + } + } } `; diff --git a/client/src/pages/scoreboard/scoreboard.page.container.jsx b/client/src/pages/scoreboard/scoreboard.page.container.jsx index 1abffe30e..a15ad12ac 100644 --- a/client/src/pages/scoreboard/scoreboard.page.container.jsx +++ b/client/src/pages/scoreboard/scoreboard.page.container.jsx @@ -1,19 +1,21 @@ -import Icon from "@ant-design/icons"; +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 { FaHardHat, FaRegStickyNote, FaShieldAlt } from "react-icons/fa"; -import ScoreboardTimeTickets from "../../components/scoreboard-timetickets/scoreboard-timetickets.component"; +import queryString from "query-string"; +import { useHistory, useLocation } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -26,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"); @@ -41,12 +45,21 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) { return ( - + { + searchParams.tab = key; + history.push({ + search: queryString.stringify(searchParams), + }); + }} + > - {t("menus.jobsdetail.general")} + {t("scoreboard.labels.jobs")} } destroyInactiveTabPane @@ -57,8 +70,8 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) { - - {t("menus.jobsdetail.general")} + + {t("scoreboard.labels.timetickets")} } destroyInactiveTabPane diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 9f42c9251..ba02ecc71 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2426,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" diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 2c26b4a3b..ae5d81de2 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2426,9 +2426,18 @@ "asoftodaytarget": "", "dailyactual": "", "dailytarget": "", + "jobs": "", + "lastmonth": "", + "lastweek": "", "monthlytarget": "", + "productivestatistics": "", + "productivetimeticketsoverdate": "", "targets": "", + "thismonth": "", + "thisweek": "", + "timetickets": "", "todateactual": "", + "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", "workingdays": "" diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 2f2547393..b71d833e8 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2426,9 +2426,18 @@ "asoftodaytarget": "", "dailyactual": "", "dailytarget": "", + "jobs": "", + "lastmonth": "", + "lastweek": "", "monthlytarget": "", + "productivestatistics": "", + "productivetimeticketsoverdate": "", "targets": "", + "thismonth": "", + "thisweek": "", + "timetickets": "", "todateactual": "", + "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", "workingdays": "" From bd6553f8e45795033a2c3fe5e3992b9ecf8697ce Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 6 Jun 2022 10:59:17 -0700 Subject: [PATCH 11/18] Resolve AR Account Reference. --- server/accounting/qbxml/qbxml-receivables.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 78c7389f7..29af650da 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -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"), From 3666b7cd22fcebf4dbcc125b917a250eb64dc7e6 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 12:04:47 -0700 Subject: [PATCH 12/18] IO-1911 Tickets scoreboard updates. --- bodyshop_translations.babel | 52 +++++++----------- .../scoreboard-entry-edit.component.jsx | 2 +- ...scoreboard-timetickets.stats.component.jsx | 55 ++++++++++++------- client/src/translations/en_us/common.json | 8 +-- client/src/translations/es/common.json | 6 +- client/src/translations/fr/common.json | 6 +- 6 files changed, 60 insertions(+), 69 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 22823dc7c..b0565a686 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -40785,6 +40785,27 @@ + + calendarperiod + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dailyactual false @@ -41214,37 +41235,6 @@ - - scoredboard - - - successes - - - updated - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - - - - - tech diff --git a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx index 3a74409f2..a8488e9dd 100644 --- a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx +++ b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx @@ -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); } diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx index 863f5f14f..71492893c 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -1,4 +1,4 @@ -import { Card, Col, Row, Space, Statistic, Table } from "antd"; +import { Card, Col, Row, Space, Statistic, Table, Typography } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -68,27 +68,40 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { return ( - - - - - - - + + + + + + + + + + + + + + + + + + + {t("scoreboard.labels.calendarperiod")} + - +
Date: Mon, 6 Jun 2022 12:10:01 -0700 Subject: [PATCH 13/18] IO-1911 Add scrolling to time tickets scoreboard. --- .../scoreboard-timetickets.stats.component.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx index 71492893c..8b521e990 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -106,6 +106,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { columns={columns} dataSource={tableData} id="employee_number" + scroll={{ y: "300px" }} /> From a0b238c4bb867a5576217d5773253b0f6fc811c4 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 12:20:32 -0700 Subject: [PATCH 14/18] IO-1841 Remove special characters on QB Export. --- .../scoreboard-timetickets.stats.component.jsx | 2 +- server/accounting/qbo/qbo-receivables.js | 5 +++-- server/accounting/qbxml/qbxml-utils.js | 12 ++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx index 8b521e990..6d2183a54 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -1,4 +1,4 @@ -import { Card, Col, Row, Space, Statistic, Table, Typography } from "antd"; +import { Card, Col, Row, Statistic, Table, Typography } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index bab135c25..fec6a15c9 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -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({ diff --git a/server/accounting/qbxml/qbxml-utils.js b/server/accounting/qbxml/qbxml-utils.js index 3cee63ca8..af59b3cf7 100644 --- a/server/accounting/qbxml/qbxml-utils.js +++ b/server/accounting/qbxml/qbxml-utils.js @@ -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(":", " "); } } }; From 320aa9c1778fa91dde829b0099f7875ea3936d15 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 12:32:32 -0700 Subject: [PATCH 15/18] IO-1923 Add CC warning to CC list page. --- .../courtesy-cars-list.component.jsx | 26 ++++++++++++++++--- client/src/graphql/courtesy-car.queries.js | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index a488368b9..62f6d1119 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -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 ( + + {t(record.status)} + {(mileageOver || dueForService) && ( + + + + )} + + ); + }, }, { title: t("courtesycars.fields.year"), diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index d4e19bac8..a5bcbea55 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -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 } From 644f2696293f7986529e7179fa8ed39361b15e62 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 12:59:24 -0700 Subject: [PATCH 16/18] IO-1919 Add filtering to parts queue. --- .../parts-queue.page.component.jsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client/src/pages/parts-queue/parts-queue.page.component.jsx b/client/src/pages/parts-queue/parts-queue.page.component.jsx index b0b66cbf1..bc6bf2c03 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.component.jsx @@ -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) => ( Date: Mon, 6 Jun 2022 13:53:09 -0700 Subject: [PATCH 17/18] IO-1920 Add ability to directly post tickets from tech console. --- bodyshop_translations.babel | 26 ++++++++++++++++++ .../tech-job-clock-in-form.container.jsx | 27 ++++++++++++++++--- .../time-ticket-modal.component.jsx | 2 ++ .../time-ticket-modal.container.jsx | 6 +++++ client/src/translations/en_us/common.json | 3 +++ client/src/translations/es/common.json | 3 +++ client/src/translations/fr/common.json | 3 +++ 7 files changed, 67 insertions(+), 3 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index b0565a686..516cab7d9 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -5691,6 +5691,32 @@ + + inventory + + + list + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobs diff --git a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx index cae229746..4144d06d3 100644 --- a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx +++ b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx @@ -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={ +