diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index ad5db59a6..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 @@ -17213,6 +17239,241 @@ + + 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 + + + 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 + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + showall + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + showavailable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + successes + + + inserted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + joblines @@ -29934,6 +30195,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false @@ -40529,6 +40811,27 @@ + + calendarperiod + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dailyactual false @@ -40571,6 +40874,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 @@ -40592,6 +40958,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 @@ -40613,6 +41021,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 @@ -40634,6 +41105,27 @@ + + totaloverperiod + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + weeklyactual false @@ -40769,37 +41261,6 @@ - - scoredboard - - - successes - - - updated - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - - - - - tech @@ -42383,6 +42844,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false @@ -43204,6 +43686,27 @@ + + inventory + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs 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 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..a9eb07443 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 && !billEdit) { + loadInventory(); + } }, [ form, + billEdit, loadOutstandingReturns, + loadInventory, setDiscount, vendorAutoCompleteOptions, loadLines, + bodyshop.inhousevendorid, ]); return ( @@ -425,6 +434,7 @@ export function BillFormComponent({ form={form} responsibilityCenters={responsibilityCenters} disabled={disabled} + billEdit={billEdit} /> )} diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx index 4fe056667..358bb7f38 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 && ( )} + {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..18d996d5a 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,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import CiecaSelect from "../../utils/Ciecaselect"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; +import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; +import { useTreatments } from "@splitsoftware/splitio-react"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -34,10 +36,16 @@ export function BillEnterModalLinesComponent({ discount, form, responsibilityCenters, + billEdit, + billid, }) { const { t } = useTranslation(); const { setFieldsValue, getFieldsValue, getFieldValue } = form; - + const { Simple_Inventory } = useTreatments( + ["Simple_Inventory"], + {}, + bodyshop && bodyshop.imexshopid + ); const columns = (remove) => { return [ { @@ -477,9 +485,22 @@ export function BillEnterModalLinesComponent({ dataIndex: "actions", render: (text, record) => ( - + + + + {() => + Simple_Inventory.treatment === "on" && ( + + ) + } + + ), }, ]; 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..2a6d76355 --- /dev/null +++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx @@ -0,0 +1,147 @@ +import { FileAddFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, notification, Tooltip } from "antd"; +import { t } from "i18next"; +import moment from "moment"; +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; +import queryString from "query-string"; +import { useLocation } from "react-router-dom"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(BilllineAddInventory); + +export function BilllineAddInventory({ + currentUser, + bodyshop, + billline, + disabled, + jobid, +}) { + const [loading, setLoading] = useState(false); + const { billid } = queryString.parse(useLocation().search); + + const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT); + + const addToInventory = async () => { + setLoading(true); + + //Check to make sure there are no existing items already in the inventory. + + const cm = { + vendorid: bodyshop.inhousevendorid, + invoice_number: "ih", + jobid: jobid, + isinhouse: true, + is_credit_memo: true, + date: moment().format("YYYY-MM-DD"), + federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, + state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, + local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, + total: 0, + billlines: [ + { + actual_price: billline.actual_price, + actual_cost: billline.actual_cost, + quantity: billline.quantity, + line_desc: billline.line_desc, + cost_center: billline.cost_center, + deductedfromlbr: billline.deductedfromlbr, + applicable_taxes: { + local: false, //billline.applicable_taxes.local, + state: false, //billline.applicable_taxes.state, + federal: false, // billline.applicable_taxes.federal, + }, + }, + ], + }; + + cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100; + + const insertResult = await insertInventoryLine({ + variables: { + joblineId: billline.joblineid, + joblineStatus: bodyshop.md_order_statuses.default_returned, + inv: { + shopid: bodyshop.id, + billlineid: billline.id, + actual_price: billline.actual_price, + actual_cost: billline.actual_cost, + quantity: billline.quantity, + line_desc: billline.line_desc, + }, + cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert. + pol: { + returnfrombill: billid, + vendorid: bodyshop.inhousevendorid, + deliver_by: moment().format("YYYY-MM-DD"), + parts_order_lines: { + data: [ + { + line_desc: billline.line_desc, + + act_price: billline.actual_price, + cost: billline.actual_cost, + quantity: billline.quantity, + job_line_id: billline.joblineid, + part_type: billline.jobline.part_type, + cm_received: true, + }, + ], + }, + order_date: "2022-06-01", + orderedby: currentUser.email, + jobid: jobid, + user_email: currentUser.email, + return: true, + status: "Ordered", + }, + }, + refetchQueries: ["QUERY_BILL_BY_PK"], + }); + + if (!insertResult.errors) { + notification.open({ + type: "success", + message: t("inventory.successes.inserted"), + }); + } else { + notification.open({ + type: "error", + message: t("inventory.errors.inserting", { + error: JSON.stringify(insertResult.errors), + }), + }); + } + + setLoading(false); + }; + + return ( + + + + ); +} 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/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 2a4e9698e..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,7 +206,20 @@ function Header({ > {t("menus.header.enterbills")} - + {Simple_Inventory.treatment === "on" && ( + <> + + } + > + + {t("menus.header.inventory")} + + + + )} + }> {t("menus.header.allpayments")} @@ -216,7 +236,6 @@ function Header({ {t("menus.header.enterpayment")} - }> {t("menus.header.timetickets")} @@ -235,7 +254,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 } = 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/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx index b1d4f33f5..701ebe1c6 100644 --- a/client/src/components/job-bills-total/job-bills-total.component.jsx +++ b/client/src/components/job-bills-total/job-bills-total.component.jsx @@ -86,6 +86,7 @@ export default function JobBillsTotalComponent({ const totalPartsSublet = Dinero(totals.parts.parts.total) .add(Dinero(totals.parts.sublets.total)) + .add(Dinero(totals.additional.shipping)) .add(Dinero(totals.additional.towing)); const discrepancy = totalPartsSublet.subtract(billTotals); diff --git a/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx b/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx index cde3abc6c..8c83a504a 100644 --- a/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx +++ b/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx @@ -22,7 +22,8 @@ export default function JobReconciliationModalComponent({ job, bills }) { (j.part_type !== null && j.part_type !== "PAE") || (j.line_desc && j.line_desc.toLowerCase().includes("towing") && - j.lbr_op === "OP13") + j.lbr_op === "OP13") || + j.db_ref === "936004" //ADD SHIPPING LINE. ); return ( diff --git a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js index 119878890..6a6e1a0f6 100644 --- a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js +++ b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js @@ -1,5 +1,6 @@ import i18next from "i18next"; import _ from "lodash"; + export const reconcileByAssocLine = ( jobLines, jobLineState, @@ -73,7 +74,12 @@ export const reconcileByPrice = ( jobLines.forEach((jl) => { const matchingBillLineIds = billLines - .filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed) + .filter( + (bl) => + bl.actual_price === jl.act_price && + bl.quantity === jl.part_qty && + !jl.removed + ) .map((bl) => bl.id); if (matchingBillLineIds.length > 1) { 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/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-targets-table/scoreboard-targets-table.util.js b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js index 97f1813b9..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 @@ -47,3 +47,15 @@ export const ListOfDaysInCurrentMonth = () => { days.push(dateEnd.format("YYYY-MM-DD")); return days; }; + +export const ListDaysBetween = ({ start, end }) => { + const days = []; + const dateStart = moment(start); + const dateEnd = moment(end); + while (dateEnd.diff(dateStart, "days") > 0) { + days.push(dateStart.format("YYYY-MM-DD")); + dateStart.add(1, "days"); + } + days.push(dateEnd.format("YYYY-MM-DD")); + return days; +}; diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx new file mode 100644 index 000000000..063638a47 --- /dev/null +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.bar.component.jsx @@ -0,0 +1,81 @@ +import { Card } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { + Bar, + CartesianGrid, + ComposedChart, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import TimeTicketsDatesSelector from "../ticket-tickets-dates-selector/time-tickets-dates-selector.component"; +const graphProps = { + strokeWidth: 3, +}; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ScoreboardTicketsBar); + +export function ScoreboardTicketsBar({ data, bodyshop }) { + const { t } = useTranslation(); + return ( + } + > + + + + + + + + {/* */} + {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 new file mode 100644 index 000000000..f85982359 --- /dev/null +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -0,0 +1,301 @@ +import { useQuery } from "@apollo/client"; +import { Col, Row } from "antd"; +import _ from "lodash"; +import moment from "moment"; +import queryString from "query-string"; +import React, { useMemo } from "react"; +import { useLocation } from "react-router-dom"; +import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries"; +import AlertComponent from "../alert/alert.component"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util"; +import ScoreboardTicketsBar from "./scoreboard-timetickets.bar.component"; +import ScoreboardTicketsStats from "./scoreboard-timetickets.stats.component"; + +export default function ScoreboardTimeTickets() { + const searchParams = queryString.parse(useLocation().search); + const { start, end } = searchParams; + const startDate = start + ? moment(start) + : moment().startOf("week").subtract(7, "days"); + const endDate = end ? moment(end) : moment().endOf("week"); + + const fixedPeriods = useMemo(() => { + const endOfThisMonth = moment().endOf("month"); + const startofthisMonth = moment().startOf("month"); + + const endOfLastmonth = moment().subtract(1, "month").endOf("month"); + const startOfLastmonth = moment().subtract(1, "month").startOf("month"); + + const endOfThisWeek = moment().endOf("week"); + const startOfThisWeek = moment().startOf("week"); + + const endOfLastWeek = moment().subtract(1, "week").endOf("week"); + const startOfLastWeek = moment().subtract(1, "week").startOf("week"); + + const allDates = [ + endOfThisMonth, + startofthisMonth, + endOfLastmonth, + startOfLastmonth, + endOfThisWeek, + startOfThisWeek, + endOfLastWeek, + startOfLastWeek, + ]; + const start = moment.min(allDates); + const end = moment.max(allDates); + return { + start, + end, + endOfThisMonth, + startofthisMonth, + endOfLastmonth, + startOfLastmonth, + endOfThisWeek, + startOfThisWeek, + endOfLastWeek, + startOfLastWeek, + }; + }, []); + + const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE, { + variables: { + start: startDate.format("YYYY-MM-DD"), + end: endDate.format("YYYY-MM-DD"), + fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), + fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + pollInterval: 60000, + skip: !fixedPeriods, + }); + + const calculatedData = useMemo(() => { + if (!data) return []; + const ret = { + totalThisWeek: 0, + totalLastWeek: 0, + totalThisMonth: 0, + totalLastMonth: 0, + totalOverPeriod: 0, + employees: {}, + }; + data.fixedperiod.forEach((ticket) => { + const ticketDate = moment(ticket.date); + + if (!ret.employees[ticket.employee.employee_number]) { + ret.employees[ticket.employee.employee_number] = { + totalThisWeek: 0, + totalLastWeek: 0, + totalThisMonth: 0, + totalLastMonth: 0, + totalOverPeriod: 0, + }; + } + + if ( + ticketDate.isBetween( + fixedPeriods.startOfThisWeek, + fixedPeriods.endOfThisWeek, + undefined, + "[]" + ) + ) { + ret.totalThisWeek = ret.totalThisWeek + ticket.productivehrs; + ret.employees[ticket.employee.employee_number].totalThisWeek = + ret.employees[ticket.employee.employee_number].totalThisWeek + + ticket.productivehrs; + } else if ( + ticketDate.isBetween( + fixedPeriods.startOfLastWeek, + fixedPeriods.endOfLastWeek, + undefined, + "[]" + ) + ) { + ret.totalLastWeek = ret.totalLastWeek + ticket.productivehrs; + ret.employees[ticket.employee.employee_number].totalLastWeek = + ret.employees[ticket.employee.employee_number].totalLastWeek + + ticket.productivehrs; + } + if ( + ticketDate.isBetween( + fixedPeriods.startofthisMonth, + fixedPeriods.endOfThisMonth, + undefined, + "[]" + ) + ) { + ret.totalThisMonth = ret.totalThisMonth + ticket.productivehrs; + ret.employees[ticket.employee.employee_number].totalThisMonth = + ret.employees[ticket.employee.employee_number].totalThisMonth + + ticket.productivehrs; + } else if ( + ticketDate.isBetween( + fixedPeriods.startOfLastmonth, + fixedPeriods.endOfLastmonth, + undefined, + "[]" + ) + ) { + ret.totalLastMonth = ret.totalLastMonth + ticket.productivehrs; + ret.employees[ticket.employee.employee_number].totalLastMonth = + ret.employees[ticket.employee.employee_number].totalLastMonth + + ticket.productivehrs; + } + }); + + const ticketsGroupedByDate = _.groupBy(data.timetickets, "date"); + const listOfDays = Utils.ListDaysBetween({ + start: startDate, + end: endDate, + }); + + const employees = []; + const ret2 = []; + let totals = { + totalproductive: 0, + totalactual: 0, + employees: {}, + }; + + listOfDays.forEach((day) => { + const r = { + date: moment(day).format("MM/DD"), + actualtotal: 0, + productivetotal: 0, + employees: {}, + }; + + if (ticketsGroupedByDate[day]) { + ticketsGroupedByDate[day].forEach((ticket) => { + r.actualtotal = r.actualtotal + ticket.actualhrs; + r.productivetotal = r.productivetotal + ticket.productivehrs; + totals.totalactual = totals.totalactual + ticket.actualhrs; + totals.totalproductive = + totals.totalproductive + ticket.productivehrs; + + employees.push(ticket.employee.employee_number); + //Add to table data. + ret.employees[ticket.employee.employee_number].totalOverPeriod = + ret.employees[ticket.employee.employee_number].totalOverPeriod + + ticket.productivehrs; + + if (!totals.employees[ticket.employee.employee_number]) + totals.employees[ticket.employee.employee_number] = { + totalactual: 0, + totalproductive: 0, + }; + + if (!r.employees[ticket.employee.employee_number]) + r.employees[ticket.employee.employee_number] = { + actual: 0, + productive: 0, + }; + + //Add to totals. + totals.employees[ticket.employee.employee_number].totalproductive = + totals.employees[ticket.employee.employee_number].totalproductive + + ticket.productivehrs; + + totals.employees[ticket.employee.employee_number].totalactual = + totals.employees[ticket.employee.employee_number].totalactual + + ticket.actualhrs; + //Add to dailys. + r.employees[ticket.employee.employee_number].productive = + r.employees[ticket.employee.employee_number].productive + + ticket.productivehrs; + + r.employees[ticket.employee.employee_number].actual = + r.employees[ticket.employee.employee_number].actual + + ticket.actualhrs; + }); + } + + ret2.push(r); + }); + + return { + fixed: ret, + timeperiod: { + totals, + chartData: ret2, + employees: _.uniq(employees), + colors: getColorArray(employees.length), + }, + }; + }, [fixedPeriods, data, startDate, endDate]); + + if (error) return ; + if (loading) return ; + return ( + + + + + + + + + ); +} + +//Include a filter by employee. + +//Hours produced today. +//Hours produced in last 7 days +//Hours produced for time period by day +//Hours produced by employee by day for time period. + +function getColorArray(num) { + return [ + "#3366cc", + "#dc3912", + "#ff9900", + "#109618", + "#990099", + "#0099c6", + "#dd4477", + "#66aa00", + "#b82e2e", + "#316395", + "#3366cc", + "#994499", + "#22aa99", + "#aaaa11", + "#6633cc", + "#e67300", + "#8b0707", + "#651067", + "#329262", + "#5574a6", + "#3b3eac", + "#b77322", + "#16d620", + "#b91383", + "#f4359e", + "#9c5935", + "#a9c413", + "#2a778d", + "#668d1c", + "#bea413", + "#0c5922", + "#743411", + ]; + // var result = []; + // for (var i = 0; i < num; i += 1) { + // var letters = "0123456789ABCDEF".split(""); + // var color = "#"; + // for (var j = 0; j < 6; j += 1) { + // color += letters[Math.floor(Math.random() * 16)]; + // } + // result.push(color); + // } + // return result; +} 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..6d2183a54 --- /dev/null +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -0,0 +1,115 @@ +import { Card, Col, Row, Statistic, Table, Typography } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ScoreboardTicketsStats); + +export function ScoreboardTicketsStats({ data, bodyshop }) { + const { t } = useTranslation(); + + const columns = [ + { + title: t("employees.fields.employee_number"), + dataIndex: "employee_number", + key: "employee_number", + sorter: (a, b) => a.employee_number - b.employee_number, + }, + { + title: t("scoreboard.labels.thisweek"), + dataIndex: "totalThisWeek", + key: "totalThisWeek", + sorter: (a, b) => a.totalThisWeek - b.totalThisWeek, + }, + { + title: t("scoreboard.labels.lastweek"), + dataIndex: "totalLastWeek", + key: "totalLastWeek", + sorter: (a, b) => a.totalLastWeek - b.totalLastWeek, + }, + { + title: t("scoreboard.labels.thismonth"), + dataIndex: "totalThisMonth", + key: "totalThisMonth", + sorter: (a, b) => a.totalThisMonth - b.totalThisMonth, + }, + { + title: t("scoreboard.labels.lastmonth"), + dataIndex: "totalLastMonth", + key: "totalLastMonth", + sorter: (a, b) => a.totalLastMonth - b.totalLastMonth, + }, + { + title: t("scoreboard.labels.totaloverperiod"), + dataIndex: "totalOverPeriod", + key: "totalOverPeriod", + sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod, + }, + ]; + + const tableData = data + ? Object.keys(data.employees).map((key) => { + return { employee_number: key, ...data.employees[key] }; + }) + : []; + + return ( + + + + + + + + + + + + + + + + + + + + + {t("scoreboard.labels.calendarperiod")} + + + +
+ + + + ); +} 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/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={ +