From 96995fdd1bca5037a44d457233bb4f772aab67fa Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 30 May 2022 16:30:49 -0700 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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 3666b7cd22fcebf4dbcc125b917a250eb64dc7e6 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 12:04:47 -0700 Subject: [PATCH 11/41] 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 12/41] 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 13/41] 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 14/41] 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 15/41] 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 16/41] 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={ + - - } + )} + + + } + > +
setVisible(true)} + style={{ + height: "19px", + }} + className={className} > -
setVisible(true)} - style={{ - height: "19px", - }} - className={className} - > - {record[field]} -
- -
+ {record[field]} + + ); } diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index 18c2e633d..134095c78 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -17,6 +17,11 @@ export function DateTimeFormatter(props) { ) : null; } +export function TimeFormatter(props) { + return props.children + ? moment(props.children).format(props.format ? props.format : "hh:mm a") + : null; +} export function TimeAgoFormatter(props) { const m = moment(props.children); From d6c8d97715a16145e2c71be67991d29d400f12f1 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 6 Jun 2022 17:30:20 -0700 Subject: [PATCH 19/41] Add comments field to parts queue. --- .../job-parts-queue-count.component.jsx | 4 ++-- .../production-list-columns.comment.component.jsx | 5 +++-- client/src/graphql/jobs.queries.js | 1 + .../parts-queue/parts-queue.page.component.jsx | 14 ++++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx b/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx index 9ead0530c..fe394f301 100644 --- a/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx +++ b/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx @@ -11,7 +11,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount); -export function JobPartsQueueCount({ bodyshop, parts }) { +export function JobPartsQueueCount({ bodyshop, parts, style }) { const partsStatus = useMemo(() => { if (!parts) return null; return parts.reduce( @@ -36,7 +36,7 @@ export function JobPartsQueueCount({ bodyshop, parts }) { if (!parts) return null; return ( - +
{partsStatus.total} diff --git a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx index fafccff93..d66f72c75 100644 --- a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx @@ -1,6 +1,6 @@ import Icon from "@ant-design/icons"; import { useMutation } from "@apollo/client"; -import { Button, Input, Popover } from "antd"; +import { Button, Input, Popover, Tooltip } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { FaRegStickyNote } from "react-icons/fa"; @@ -69,10 +69,11 @@ export default function ProductionListColumnComment({ record }) { cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", + display: "inline-block", }} > - {record.comment || " "} + {record.comment || " "} ); diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index b5611eab7..eba649d29 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -85,6 +85,7 @@ export const QUERY_PARTS_QUEUE = gql` vehicleid ownerid queued_for_parts + comment joblines_status { count part_type 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 bc6bf2c03..492ee1955 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.component.jsx @@ -11,6 +11,7 @@ import AlertComponent from "../../components/alert/alert.component"; import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component"; import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component"; import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component"; +import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { onlyUnique } from "../../utils/arrayHelper"; @@ -228,9 +229,18 @@ export function PartsQueuePageComponent({ bodyshop }) { dataIndex: "partsstatus", key: "partsstatus", render: (text, record) => ( - + ), }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + render: (text, record) => , + }, { title: t("jobs.fields.queued_for_parts"), dataIndex: "queued_for_parts", @@ -247,7 +257,7 @@ export function PartsQueuePageComponent({ bodyshop }) { value: false, }, ], - //onFilter: (value, record) => record.queued_for_parts === value, + onFilter: (value, record) => record.queued_for_parts === value, render: (text, record) => ( Date: Tue, 7 Jun 2022 08:37:27 -0700 Subject: [PATCH 20/41] IO-1925 Change job costing to have negative sale on adjustment. --- server/job/job-costing.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/job/job-costing.js b/server/job/job-costing.js index 5ccdeba0d..13f9201cc 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -781,33 +781,33 @@ function GenerateCostingData(job) { //Push adjustments to bottom line. if (job.adjustment_bottom_line) { //Add to totals. - const Adjustment = Dinero({ amount: job.adjustment_bottom_line * -100 }); //Need to invert, since this is being assigned as a cost. - summaryData.totalLaborCost = summaryData.totalLaborCost.add(Adjustment); - summaryData.totalCost = summaryData.totalCost.add(Adjustment); + const Adjustment = Dinero({ amount: job.adjustment_bottom_line * 100 }); //Need to invert, since this is being assigned as a cost. + summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment); + summaryData.totalSales = summaryData.totalSales.add(Adjustment); //Add to lines. costCenterData.push({ id: "Adj", cost_center: "Adjustment", - sale_labor: Dinero().toFormat(), - sale_labor_dinero: Dinero(), + sale_labor: Adjustment.toFormat(), + sale_labor_dinero: Adjustment, sale_parts: Dinero().toFormat(), sale_parts_dinero: Dinero(), sale_additional: Dinero(), sale_additional_dinero: Dinero(), sale_sublet: Dinero(), sale_sublet_dinero: Dinero(), - sales: Dinero().toFormat(), - sales_dinero: Dinero(), + sales: Adjustment.toFormat(), + sales_dinero: Adjustment, cost_parts: Dinero().toFormat(), cost_parts_dinero: Dinero(), - cost_labor: Adjustment.toFormat(), - cost_labor_dinero: Adjustment, + cost_labor: Dinero().toFormat(), //Adjustment.toFormat(), + cost_labor_dinero: Dinero(), // Adjustment, cost_additional: Dinero(), cost_additional_dinero: Dinero(), cost_sublet: Dinero(), cost_sublet_dinero: Dinero(), - costs: Adjustment.toFormat(), - costs_dinero: Adjustment, + costs: Dinero().toFormat(), + costs_dinero: Dinero(), gpdollars_dinero: Dinero(), gpdollars: Dinero().toFormat(), gppercent: formatGpPercent(0), From 9491d5f06986249ae96177a97b6c1cc02569a361 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 7 Jun 2022 08:43:24 -0700 Subject: [PATCH 21/41] IO-1916 Resolve required labels on sign in. --- .../components/sign-in-form/sign-in-form.component.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/components/sign-in-form/sign-in-form.component.jsx b/client/src/components/sign-in-form/sign-in-form.component.jsx index da1c749f7..c964c0dc2 100644 --- a/client/src/components/sign-in-form/sign-in-form.component.jsx +++ b/client/src/components/sign-in-form/sign-in-form.component.jsx @@ -60,7 +60,10 @@ export function SignInComponent({ Date: Tue, 7 Jun 2022 08:55:07 -0700 Subject: [PATCH 22/41] IO-1862 Add option to remove from production on invoice close. --- .../src/pages/jobs-close/jobs-close.component.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/pages/jobs-close/jobs-close.component.jsx b/client/src/pages/jobs-close/jobs-close.component.jsx index 697211ee6..6106a013a 100644 --- a/client/src/pages/jobs-close/jobs-close.component.jsx +++ b/client/src/pages/jobs-close/jobs-close.component.jsx @@ -10,6 +10,7 @@ import { PageHeader, InputNumber, Input, + Switch, } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -42,7 +43,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { const [closeJob] = useMutation(UPDATE_JOB); const [loading, setLoading] = useState(false); - const handleFinish = async (values) => { + const handleFinish = async ({ removefromproduction, ...values }) => { setLoading(true); const result = await client.mutate({ mutation: generateJobLinesUpdatesForInvoicing(values.joblines), @@ -63,6 +64,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { kmin: values.kmin, kmout: values.kmout, dms_allocation: values.dms_allocation, + ...(removefromproduction ? { inproduction: false } : {}), }, }, refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"], @@ -248,6 +250,15 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { onlyFuture={!!bodyshop.cdk_dealerid} /> + {!jobRO && ( + + + + )} {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( Date: Tue, 7 Jun 2022 08:59:28 -0700 Subject: [PATCH 23/41] IO-1732 Adjust tech job drawer width. --- .../tech-lookup-jobs-drawer.component.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx b/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx index 2d28099f0..02e62d274 100644 --- a/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx +++ b/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx @@ -43,9 +43,9 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) { xs: "100%", sm: "100%", md: "100%", - lg: "70%", - xl: "70%", - xxl: "70%", + lg: "100%", + xl: "90%", + xxl: "85%", }; const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] From fe5e2a247ae035fe05d16bb2f962e55922f3bf61 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 7 Jun 2022 11:41:27 -0700 Subject: [PATCH 24/41] IO-1914 Inventory bugfixes. --- bodyshop_translations.babel | 21 +++++++++++ .../bill-delete-button.component.jsx | 24 +++++++++---- .../bill-form/bill-form.lines.component.jsx | 35 ++++++++++++------- .../billline-add-inventory.component.jsx | 15 ++++---- client/src/redux/media/media.actions.js | 1 - client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 8 files changed, 74 insertions(+), 25 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 516cab7d9..31b297cd9 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -2181,6 +2181,27 @@ + + existinginventoryline + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + exporting false diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx index eee5f522b..b6cb19992 100644 --- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx +++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx @@ -15,7 +15,8 @@ export default function BillDeleteButton({ bill }) { setLoading(true); const result = await deleteBill({ variables: { billId: bill.id }, - update(cache) { + update(cache, { errors }) { + if (errors) return; cache.modify({ fields: { bills(existingBills, { readField }) { @@ -36,11 +37,22 @@ export default function BillDeleteButton({ bill }) { if (!!!result.errors) { notification["success"]({ message: t("bills.successes.deleted") }); } else { - notification["error"]({ - message: t("bills.errors.deleting", { - error: JSON.stringify(result.errors), - }), - }); + //Check if it's an fkey violation. + const error = JSON.stringify(result.errors); + + if (error.toLowerCase().includes("inventory_billid_fkey")) { + notification["error"]({ + message: t("bills.errors.deleting", { + error: t("bills.errors.existinginventoryline"), + }), + }); + } else { + notification["error"]({ + message: t("bills.errors.deleting", { + error: JSON.stringify(result.errors), + }), + }); + } } setLoading(false); 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 18d996d5a..71a0e3b72 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -485,22 +485,33 @@ export function BillEnterModalLinesComponent({ dataIndex: "actions", render: (text, record) => ( - - - - {() => - 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 2a6d76355..d2cbe92bc 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 @@ -64,9 +64,9 @@ export function BilllineAddInventory({ cost_center: billline.cost_center, deductedfromlbr: billline.deductedfromlbr, applicable_taxes: { - local: false, //billline.applicable_taxes.local, - state: false, //billline.applicable_taxes.state, - federal: false, // billline.applicable_taxes.federal, + local: billline.applicable_taxes.local, + state: billline.applicable_taxes.state, + federal: billline.applicable_taxes.federal, }, }, ], @@ -76,7 +76,9 @@ export function BilllineAddInventory({ const insertResult = await insertInventoryLine({ variables: { - joblineId: billline.joblineid, + joblineId: + billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line. + //Unfortunately, we can't send null as the GQL syntax validation fails. joblineStatus: bodyshop.md_order_statuses.default_returned, inv: { shopid: bodyshop.id, @@ -99,8 +101,9 @@ export function BilllineAddInventory({ act_price: billline.actual_price, cost: billline.actual_cost, quantity: billline.quantity, - job_line_id: billline.joblineid, - part_type: billline.jobline.part_type, + job_line_id: + billline.joblineid === "noline" ? null : billline.joblineid, + part_type: billline.jobline && billline.jobline.part_type, cm_received: true, }, ], diff --git a/client/src/redux/media/media.actions.js b/client/src/redux/media/media.actions.js index 1ff5e943c..3d0827123 100644 --- a/client/src/redux/media/media.actions.js +++ b/client/src/redux/media/media.actions.js @@ -6,7 +6,6 @@ export const getJobMedia = (jobid) => ({ }); export const getBillMedia = ({ jobid, invoice_number }) => { - console.log("in the action"); return { type: MediaActionTypes.GET_MEDIA_FOR_BILL, payload: { jobid, invoice_number }, diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index f283e1777..c55a48010 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -147,6 +147,7 @@ "errors": { "creating": "Error adding bill. {{error}}", "deleting": "Error deleting bill. {{error}}", + "existinginventoryline": "This bill cannot be deleted as it is tied to items in inventory.", "exporting": "Error exporting payable(s). {{error}}", "exporting-partner": "Unable to connect to ImEX Partner. Please ensure it is running and logged in.", "invalidro": "Not a valid RO.", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index b40b5175d..ca89fc4d9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -147,6 +147,7 @@ "errors": { "creating": "", "deleting": "", + "existinginventoryline": "", "exporting": "", "exporting-partner": "", "invalidro": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 6843f664f..bd63176f0 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -147,6 +147,7 @@ "errors": { "creating": "", "deleting": "", + "existinginventoryline": "", "exporting": "", "exporting-partner": "", "invalidro": "", From d32fd9e69711496b156c37993782740e71949113 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 7 Jun 2022 12:14:24 -0700 Subject: [PATCH 25/41] IO-1914 Consume from inventory screen. --- bodyshop_translations.babel | 21 ++++++ .../bill-inventory-table.component.jsx | 14 +++- .../inventory-bill-ro.component.jsx | 65 +++++++++++++++++++ .../inventory-list.component.jsx | 12 +++- client/src/graphql/inventory.queries.js | 5 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 8 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 31b297cd9..4101a134e 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -17287,6 +17287,27 @@ + + addtoro + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + consumefrominventory false 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 index d8fb121d0..e9403a0f1 100644 --- a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx +++ b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx @@ -7,9 +7,11 @@ import "./bill-inventory-table.styles.scss"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, + billEnterModal: selectBillEnterModal, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -17,6 +19,7 @@ const mapDispatchToProps = (dispatch) => ({ export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable); export function BillInventoryTable({ + billEnterModal, bodyshop, form, billEdit, @@ -28,11 +31,18 @@ export function BillInventoryTable({ useEffect(() => { if (inventoryData) { form.setFieldsValue({ - inventory: inventoryData.inventory, + inventory: billEnterModal.context.consumeinventoryid + ? inventoryData.inventory.map((i) => { + if (i.id === billEnterModal.context.consumeinventoryid) + i.consumefrominventory = true; + return i; + }) + : inventoryData.inventory, }); } - }, [inventoryData, form]); + }, [inventoryData, form, billEnterModal.context.consumeinventoryid]); + console.log(form.getFieldsValue()); return ( prev.vendorid !== cur.vendorid} diff --git a/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx b/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx new file mode 100644 index 000000000..f83811ed7 --- /dev/null +++ b/client/src/components/inventory-bill-ro/inventory-bill-ro.component.jsx @@ -0,0 +1,65 @@ +import { Button } from "antd"; +import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import moment from "moment"; +import { useTranslation } from "react-i18next"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + setBillEnterContext: (context) => + dispatch(setModalContext({ context: context, modal: "billEnter" })), +}); +export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo); +export function InventoryBillRo({ + bodyshop, + setBillEnterContext, + inventoryline, +}) { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/client/src/components/inventory-list/inventory-list.component.jsx b/client/src/components/inventory-list/inventory-list.component.jsx index 58210eb94..95dcdea13 100644 --- a/client/src/components/inventory-list/inventory-list.component.jsx +++ b/client/src/components/inventory-list/inventory-list.component.jsx @@ -4,10 +4,11 @@ import queryString from "query-string"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { useHistory, useLocation } from "react-router-dom"; +import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, @@ -75,7 +76,14 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { key: "consumedbyjob", ellipsis: true, - render: (text, record) => record.bill?.job?.ro_number, + render: (text, record) => + record.bill?.job?.ro_number ? ( + + {record.bill?.job?.ro_number} + + ) : ( + + ), }, ]; diff --git a/client/src/graphql/inventory.queries.js b/client/src/graphql/inventory.queries.js index 51b713e92..51efcd3b8 100644 --- a/client/src/graphql/inventory.queries.js +++ b/client/src/graphql/inventory.queries.js @@ -42,7 +42,10 @@ export const UPDATE_INVENTORY_LINES = gql` export const QUERY_OUTSTANDING_INVENTORY = gql` query QUERY_OUTSTANDING_INVENTORY { - inventory(where: { consumedbybillid: { _is_null: true } }) { + inventory( + where: { consumedbybillid: { _is_null: true } } + order_by: { line_desc: asc } + ) { id actual_cost actual_price diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c55a48010..c40bfe2be 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1076,6 +1076,7 @@ "inventory": { "actions": { "addtoinventory": "Add to Inventory", + "addtoro": "Add to RO", "consumefrominventory": "Consume from Inventory?" }, "errors": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index ca89fc4d9..2793b4bf1 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1076,6 +1076,7 @@ "inventory": { "actions": { "addtoinventory": "", + "addtoro": "", "consumefrominventory": "" }, "errors": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index bd63176f0..b7011de21 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1076,6 +1076,7 @@ "inventory": { "actions": { "addtoinventory": "", + "addtoro": "", "consumefrominventory": "" }, "errors": { From a1472cd9ff59e622e01b3b48698b44136bfb587b Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 7 Jun 2022 12:39:59 -0700 Subject: [PATCH 26/41] IO-1926 Add export log to mark as exported. --- .../bill-mark-exported-button.component.jsx | 31 +++++++++++++++---- .../jobs-admin-mark-reexport.component.jsx | 29 +++++++++++++++-- ...yable-mark-selected-exported.component.jsx | 27 ++++++++++++---- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx index d81244098..80fa1ae44 100644 --- a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx +++ b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx @@ -9,11 +9,14 @@ import { createStructuredSelector } from "reselect"; import { selectAuthLevel, selectBodyshop, + selectCurrentUser, } from "../../redux/user/user.selectors"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; +import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, authLevel: selectAuthLevel, + currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -24,9 +27,15 @@ export default connect( mapDispatchToProps )(BillMarkExportedButton); -export function BillMarkExportedButton({ bodyshop, authLevel, bill }) { +export function BillMarkExportedButton({ + currentUser, + bodyshop, + authLevel, + bill, +}) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); + const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [updateBill] = useMutation(gql` mutation UPDATE_BILL($billId: uuid!) { @@ -46,6 +55,20 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) { variables: { billId: bill.id }, }); + await insertExportLog({ + variables: { + logs: [ + { + bodyshopid: bodyshop.id, + billid: bill.id, + successful: true, + message: t("general.labels.markedexported"), + useremail: currentUser.email, + }, + ], + }, + }); + if (!result.errors) { notification["success"]({ message: t("bills.successes.markexported"), @@ -69,11 +92,7 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) { if (hasAccess) return ( - ); diff --git a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx index 929c7f6d5..70dbf0a31 100644 --- a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx +++ b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx @@ -6,12 +6,17 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; import moment from "moment"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { insertAuditTrail } from "../../redux/application/application.actions"; +import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, + currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ insertAuditTrail: ({ jobid, operation }) => @@ -22,9 +27,15 @@ export default connect( mapDispatchToProps )(JobAdminMarkReexport); -export function JobAdminMarkReexport({ insertAuditTrail, bodyshop, job }) { +export function JobAdminMarkReexport({ + insertAuditTrail, + bodyshop, + currentUser, + job, +}) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); + const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [markJobForReexport] = useMutation(gql` mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) { update_jobs_by_pk( @@ -101,6 +112,20 @@ export function JobAdminMarkReexport({ insertAuditTrail, bodyshop, job }) { variables: { jobId: job.id, date_exported: moment() }, }); + await insertExportLog({ + variables: { + logs: [ + { + bodyshopid: bodyshop.id, + jobid: job.id, + successful: true, + message: t("general.labels.markedexported"), + useremail: currentUser.email, + }, + ], + }, + }); + if (!result.errors) { notification["success"]({ message: t("jobs.successes.save") }); insertAuditTrail({ diff --git a/client/src/components/payable-mark-selected-exported/payable-mark-selected-exported.component.jsx b/client/src/components/payable-mark-selected-exported/payable-mark-selected-exported.component.jsx index a18c370df..8032e03a5 100644 --- a/client/src/components/payable-mark-selected-exported/payable-mark-selected-exported.component.jsx +++ b/client/src/components/payable-mark-selected-exported/payable-mark-selected-exported.component.jsx @@ -4,14 +4,15 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { - selectAuthLevel, selectBodyshop, + selectCurrentUser, } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, - authLevel: selectAuthLevel, + currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -23,6 +24,8 @@ export default connect( )(BillMarkSelectedExported); export function BillMarkSelectedExported({ + bodyshop, + currentUser, billids, disabled, loadingCallback, @@ -31,7 +34,7 @@ export function BillMarkSelectedExported({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - + const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [updateBill] = useMutation(gql` mutation UPDATE_BILL($billIds: [uuid!]!) { update_bills(where: { id: { _in: $billIds } }, _set: { exported: true }) { @@ -49,9 +52,21 @@ export function BillMarkSelectedExported({ loadingCallback(true); const result = await updateBill({ variables: { billIds: billids }, - update(cache){ - - } + update(cache) {}, + }); + + await insertExportLog({ + variables: { + logs: billids.map((id) => { + return { + bodyshopid: bodyshop.id, + billid: id, + successful: true, + message: t("general.labels.markedexported"), + useremail: currentUser.email, + }; + }), + }, }); if (!result.errors) { From 77cbbef085515136846afb51f2e3f5362e335bbd Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 7 Jun 2022 14:16:54 -0700 Subject: [PATCH 27/41] IO-1914 Add Quantity Handling for inventory. --- bodyshop_translations.babel | 42 +++++++++++++++++++ .../bill-detail-edit.container.jsx | 2 +- .../bill-form/bill-form.lines.component.jsx | 18 ++++++++ .../billline-add-inventory.component.jsx | 7 +++- client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + client/src/utils/fcm-handler.js | 1 - 8 files changed, 73 insertions(+), 3 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 4101a134e..2bf1b9018 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -3314,6 +3314,27 @@ validation + + inventoryquantity + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + manualinhouse false @@ -15930,6 +15951,27 @@ + + markedexported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + message false 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 f2f03e90d..b9bbb791b 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 @@ -127,7 +127,7 @@ export function BillDetailEditcontainer({ }); billlines.forEach((billline) => { - const { deductedfromlbr, jobline, ...il } = billline; + const { deductedfromlbr, inventories, jobline, ...il } = billline; delete il.__typename; if (il.id) { 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 71a0e3b72..30aebdfe8 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -150,6 +150,24 @@ export function BillEnterModalLinesComponent({ required: true, //message: t("general.validation.required"), }, + ({ getFieldValue }) => ({ + validator(rule, value) { + if ( + value && + getFieldValue("billlines")[field.fieldKey]?.inventories + ?.length > value + ) { + return Promise.reject( + t("bills.validation.inventoryquantity", { + number: + getFieldValue("billlines")[field.fieldKey] + ?.inventories?.length, + }) + ); + } + return Promise.resolve(); + }, + }), ], }; }, 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 d2cbe92bc..1fea48f5b 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 @@ -140,10 +140,15 @@ export function BilllineAddInventory({ ); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c40bfe2be..94299f2e5 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -208,6 +208,7 @@ "reexport": "Bill marked for re-export." }, "validation": { + "inventoryquantity": "Quantity must be greater than or equal to what has been added to inventory ({{number}}).", "manualinhouse": "Manual posting to the in house vendor is restricted. ", "unique_invoice_number": "This invoice number has already been entered for this vendor." } @@ -992,6 +993,7 @@ "loadingapp": "Loading $t(titles.app)", "loadingshop": "Loading shop data...", "loggingin": "Authorizing...", + "markedexported": "Manually marked as exported.", "message": "Message", "monday": "Monday", "na": "N/A", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 2793b4bf1..afefc5c9b 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -208,6 +208,7 @@ "reexport": "" }, "validation": { + "inventoryquantity": "", "manualinhouse": "", "unique_invoice_number": "" } @@ -992,6 +993,7 @@ "loadingapp": "Cargando $t(titles.app)", "loadingshop": "Cargando datos de la tienda ...", "loggingin": "Iniciando sesión ...", + "markedexported": "", "message": "", "monday": "", "na": "N / A", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b7011de21..ccc887091 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -208,6 +208,7 @@ "reexport": "" }, "validation": { + "inventoryquantity": "", "manualinhouse": "", "unique_invoice_number": "" } @@ -992,6 +993,7 @@ "loadingapp": "Chargement de $t(titles.app)", "loadingshop": "Chargement des données de la boutique ...", "loggingin": "Vous connecter ...", + "markedexported": "", "message": "", "monday": "", "na": "N / A", diff --git a/client/src/utils/fcm-handler.js b/client/src/utils/fcm-handler.js index 0b4999b9c..116ae7a21 100644 --- a/client/src/utils/fcm-handler.js +++ b/client/src/utils/fcm-handler.js @@ -1,5 +1,4 @@ export default async function FcmHandler({ client, payload }) { - console.log("Handling payload type", payload); switch (payload.type) { case "messaging-inbound": client.cache.modify({ From b36b4cb213dcbf01b1366b22aed56eaf1ae79156 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 8 Jun 2022 09:37:48 -0700 Subject: [PATCH 28/41] Resolve email PDFs not generating header. --- .../email-overlay/email-overlay.component.jsx | 4 ++- client/src/utils/RenderTemplate.js | 26 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index ef9ab2772..d954cdcc3 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -139,7 +139,9 @@ export function EmailOverlayComponent({ {t("emails.labels.preview")} - {t("emails.labels.pdfcopywillbeattached")} + {bodyshop.attach_pdf_to_email && ( + {t("emails.labels.pdfcopywillbeattached")} + )} {() => { diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 2caa4556e..e9bc47f8f 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -80,8 +80,30 @@ export default async function RenderTemplate( } else { let pdf; if (bodyshop.attach_pdf_to_email) { - const pdfRequest = _.cloneDeep(reportRequest); - pdfRequest.template.recipe = "chrome-pdf"; + const pdfRequest = _.cloneDeep(reportRequest); //Updates to spread in the header details. + pdfRequest.template = { + ...pdfRequest.template, + ...{ + recipe: "chrome-pdf", + ...(!ignoreCustomMargins && { + chrome: { + marginTop: + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", + marginBottom: + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", + }, + }), + }, + }; + console.log("PDFREQ", pdfRequest); const pdfRender = await jsreport.renderAsync(pdfRequest); pdf = pdfRender.toDataURI(); } From fde0681a93f052b7c8d79f6c8e16fa70b2acd886 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 8 Jun 2022 13:24:43 -0700 Subject: [PATCH 29/41] Server Side CORS Updates. --- client/src/utils/CleanAxios.js | 2 ++ server.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/utils/CleanAxios.js b/client/src/utils/CleanAxios.js index 19b71e969..0ddc62be6 100644 --- a/client/src/utils/CleanAxios.js +++ b/client/src/utils/CleanAxios.js @@ -6,6 +6,8 @@ if (process.env.NODE_ENV === "production") { process.env.REACT_APP_AXIOS_BASE_API_URL || "https://api.imex.online/"; } +axios.defaults.withCredentials = true; + export const axiosAuthInterceptorId = axios.interceptors.request.use( async (config) => { if (!config.headers.Authorization) { diff --git a/server.js b/server.js index 952d1cc67..d3b809531 100644 --- a/server.js +++ b/server.js @@ -36,6 +36,7 @@ const io = new Server(server, { "https://www.imex.online", ], methods: ["GET", "POST"], + credentials: true, }, }); exports.io = io; @@ -48,7 +49,7 @@ app.use(bodyParser.json({ limit: "50mb" })); app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); //app.use(enforce.HTTPS({ trustProtoHeader: true })); app.use( - cors() + cors({ credentials: true }) // cors({ // credentials: true, // origin: [ From 82db7a1f14163bed8ac1e8d2184d531de2eb3685 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 8 Jun 2022 17:45:30 -0700 Subject: [PATCH 30/41] IO-1914 Add manual and edit lines of inventory. --- .../bill-inventory-table.component.jsx | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 index e9403a0f1..9b2222397 100644 --- a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx +++ b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx @@ -74,6 +74,7 @@ export function BillInventoryTable({ + @@ -136,6 +137,16 @@ export function BillInventoryTable({ + + + + {t("scoreboard.labels.calendarperiod")} diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index 0a8108571..28df9226d 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -60,7 +60,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql` `; export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` - query QUERY_TIME_TICKETS_IN_RANGE( + query QUERY_TIME_TICKETS_IN_RANGE_SB( $start: date! $end: date! $fixedStart: date! diff --git a/client/src/pages/scoreboard/scoreboard.page.container.jsx b/client/src/pages/scoreboard/scoreboard.page.container.jsx index a15ad12ac..ac35b6885 100644 --- a/client/src/pages/scoreboard/scoreboard.page.container.jsx +++ b/client/src/pages/scoreboard/scoreboard.page.container.jsx @@ -1,4 +1,4 @@ -import Icon, { BarsOutlined } from "@ant-design/icons"; +import Icon, { FieldTimeOutlined } from "@ant-design/icons"; import { Tabs } from "antd"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -70,7 +70,7 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) { - + {t("scoreboard.labels.timetickets")} }
{t("billlines.fields.quantity")} {t("billlines.fields.actual_price")} {t("billlines.fields.actual_cost")}{t("inventory.fields.comment")} {t("inventory.actions.consumefrominventory")}
+ + + + Date: Wed, 8 Jun 2022 17:45:58 -0700 Subject: [PATCH 31/41] IO-1914 Add manual inventory and edit. --- bodyshop_translations.babel | 194 ++++++++++++++++++ .../inventory-line-delete.component.jsx | 67 ++++++ .../inventory-list.component.jsx | 79 ++++++- .../inventory-list.container.jsx | 17 +- .../inventory-upsert-modal.component.jsx | 68 ++++++ .../inventory-upsert-modal.container.jsx | 126 ++++++++++++ .../components/rbac-wrapper/rbac-defaults.js | 1 + .../shop-info/shop-info.rbac.component.jsx | 63 ++++-- client/src/graphql/inventory.queries.js | 66 ++++++ client/src/pages/inventory/inventory.page.jsx | 2 + client/src/redux/modals/modals.reducer.js | 1 + client/src/redux/modals/modals.selectors.js | 4 + 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 | 21 ++ .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 4 + .../up.sql | 2 + .../down.sql | 1 + .../up.sql | 1 + 24 files changed, 738 insertions(+), 36 deletions(-) create mode 100644 client/src/components/inventory-line-delete/inventory-line-delete.component.jsx create mode 100644 client/src/components/inventory-upsert-modal/inventory-upsert-modal.component.jsx create mode 100644 client/src/components/inventory-upsert-modal/inventory-upsert-modal.container.jsx create mode 100644 hasura/migrations/1654731244827_alter_table_public_inventory_add_column_manualinvoicenumber/down.sql create mode 100644 hasura/migrations/1654731244827_alter_table_public_inventory_add_column_manualinvoicenumber/up.sql create mode 100644 hasura/migrations/1654731253232_alter_table_public_inventory_add_column_comment/down.sql create mode 100644 hasura/migrations/1654731253232_alter_table_public_inventory_add_column_comment/up.sql create mode 100644 hasura/migrations/1654731265455_alter_table_public_inventory_add_column_manualvendor/down.sql create mode 100644 hasura/migrations/1654731265455_alter_table_public_inventory_add_column_manualvendor/up.sql create mode 100644 hasura/migrations/1654733904736_alter_table_public_inventory_alter_column_quantity/down.sql create mode 100644 hasura/migrations/1654733904736_alter_table_public_inventory_alter_column_quantity/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 2bf1b9018..d6fc28db9 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -5736,6 +5736,27 @@ inventory + + delete + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + list false @@ -17371,6 +17392,48 @@ + + edit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + new + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -17399,6 +17462,74 @@ + + fields + + + comment + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + manualinvoicenumber + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + manualvendor + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + labels @@ -17423,6 +17554,27 @@ + + deleteconfirm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + frombillinvoicenumber false @@ -17533,6 +17685,27 @@ successes + + deleted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + inserted false @@ -17554,6 +17727,27 @@ + + updated + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + diff --git a/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx b/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx new file mode 100644 index 000000000..d31be9384 --- /dev/null +++ b/client/src/components/inventory-line-delete/inventory-line-delete.component.jsx @@ -0,0 +1,67 @@ +import { DeleteFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, notification, Popconfirm } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries"; +import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; + +export default function InventoryLineDelete({ + inventoryline, + disabled, + refetch, +}) { + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE); + + const handleDelete = async () => { + setLoading(true); + const result = await deleteInventoryLine({ + variables: { lineId: inventoryline.id }, + // update(cache, { errors }) { + // cache.modify({ + // fields: { + // inventory(existingInventory, { readField }) { + // console.log(existingInventory); + // return existingInventory.filter( + // (invRef) => inventoryline.id !== readField("id", invRef) + // ); + // }, + // }, + // }); + // }, + }); + + if (!!!result.errors) { + notification["success"]({ message: t("inventory.successes.deleted") }); + } else { + //Check if it's an fkey violation. + + notification["error"]({ + message: t("bills.errors.deleting", { + error: JSON.stringify(result.errors), + }), + }); + } + if (refetch) refetch(); + setLoading(false); + }; + + return ( + }> + + + + + ); +} diff --git a/client/src/components/inventory-list/inventory-list.component.jsx b/client/src/components/inventory-list/inventory-list.component.jsx index 95dcdea13..9baa66de0 100644 --- a/client/src/components/inventory-list/inventory-list.component.jsx +++ b/client/src/components/inventory-list/inventory-list.component.jsx @@ -1,4 +1,4 @@ -import { SyncOutlined } from "@ant-design/icons"; +import { EditFilled, SyncOutlined, FileAddFilled } from "@ant-design/icons"; import { Button, Card, Input, Space, Table, Typography } from "antd"; import queryString from "query-string"; import React from "react"; @@ -6,18 +6,28 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component"; +import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + setInventoryUpsertContext: (context) => + dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })), }); -export function JobsList({ bodyshop, refetch, loading, jobs, total }) { +export function JobsList({ + bodyshop, + refetch, + loading, + jobs, + total, + setInventoryUpsertContext, +}) { const search = queryString.parse(useLocation().search); const { page, sortcolumn, sortorder } = search; const history = useHistory(); @@ -31,6 +41,15 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc), sortOrder: sortcolumn === "line_desc" && sortorder, + render: (text, record) => + record.billline?.bill?.job ? ( +
+
{text}
+ {`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`} +
+ ) : ( + text + ), }, { title: t("inventory.labels.frombillinvoicenumber"), @@ -40,7 +59,12 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), //sortOrder: sortcolumn === "ownr_ln" && sortorder, - render: (text, record) => record.billline?.bill?.invoice_number, + render: (text, record) => + ( + (record.billline?.bill?.invoice_number || "") + + " " + + (record.manualinvoicenumber || "") + ).trim(), }, { title: t("inventory.labels.fromvendor"), @@ -50,7 +74,12 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), //sortOrder: sortcolumn === "ownr_ln" && sortorder, - render: (text, record) => record.billline?.bill?.vendor?.name, + render: (text, record) => + ( + (record.billline?.bill?.vendor?.name || "") + + " " + + (record.manualvendor || "") + ).trim(), }, { title: t("billlines.fields.actual_price"), @@ -70,6 +99,11 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { {record.actual_cost} ), }, + { + title: t("inventory.fields.comment"), + dataIndex: "comment", + key: "comment", + }, { title: t("inventory.labels.consumedbyjob"), dataIndex: "consumedbyjob", @@ -85,6 +119,30 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { ), }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + + ellipsis: true, + render: (text, record) => ( + + + + + ), + }, ]; const handleTableChange = (pagination, filters, sorter) => { @@ -113,7 +171,16 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { )} - +