diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 44b8cd815..579d11b7b 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -79,6 +79,18 @@ export function BillFormComponent({ }); }; + const handleFederalTaxExemptSwitchToggle = (checked) => { + if (checked) { + const values = form.getFieldsValue("billlines"); + if (values && values.billlines && values.billlines.length > 0) { + values.billlines.forEach((b) => { + b.applicable_taxes.federal = false; + }); + } + form.setFieldsValue({ billlines: values.billlines }); + } + }; + useEffect(() => { if (job) form.validateFields(["is_credit_memo"]); }, [job, form]); @@ -387,7 +399,16 @@ export function BillFormComponent({ > - + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + + + + ) : null} + {() => { const values = form.getFieldsValue([ "billlines", @@ -405,7 +426,7 @@ export function BillFormComponent({ totals = CalculateBillTotal(values); if (!!totals) return ( -
+
({ -}); - -export function JobsList({bodyshop,}) { - const search = queryString.parse(useLocation().search); - const [openSearchResults, setOpenSearchResults] = useState([]); - const [searchLoading, setSearchLoading] = useState(false); - const {page, selected, sortorder, sortcolumn} = search; - +export function JobsList({ bodyshop }) { + const searchParams = queryString.parse(useLocation().search); + const { selected } = searchParams; const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) .filter((screen) => !!screen[1]) .slice(-1)[0]; - - const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { + const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { variables: { - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - order: [ - { - [sortcolumn || "ro_number"]: - sortorder && sortorder !== "false" - ? (sortorder === "descend" - ? "desc" - : "asc") - : "desc", - }, - ], }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", }); - const total = data?.jobs_aggregate?.aggregate?.count || 0; - const [state, setState] = useState({ - filteredInfo: {text: ""}, + sortedInfo: {}, + filteredInfo: { text: "" }, }); - const {t} = useTranslation(); + const { t } = useTranslation(); const history = useHistory(); + const [searchText, setSearchText] = useState(""); - const jobs = data?.jobs || []; + if (error) return ; + + const jobs = data + ? searchText === "" + ? data.jobs + : data.jobs.filter( + (j) => + (j.ro_number || "") + .toString() + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_co_nm || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.comments || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || + (j.plate_no || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_model_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_make_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) + ) + : []; const handleTableChange = (pagination, filters, sorter) => { - // TODO: Is this needed? - setState({...state, filteredInfo: filters}); - - search.page = pagination.current; - search.sortcolumn = sorter.column && sorter.column.key; - search.sortorder = sorter.order; - - if (filters.status) { - search.statusFilters = JSON.stringify(flattenDeep(filters.status)); - } else { - delete search.statusFilters; - } - - history.push({search: queryString.stringify(search)}); + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; - useEffect(() => { - if (search.search && search.search.trim() !== "") { - searchJobs().catch(e => { - console.error('Something went wrong searching for jobs in the job-list component', e); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (error) return ; - - async function searchJobs(value) { - try { - setSearchLoading(true); - const searchData = await axios.post("/search", { - search: value || search.search, - index: "jobs", - }); - setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source)); - } catch (error) { - console.log("Error while fetching search results", error); - } finally { - setSearchLoading(false); - } - } - const handleOnRowClick = (record) => { if (record) { if (record.id) { history.push({ search: queryString.stringify({ - ...search, + ...searchParams, selected: record.id, }), }); @@ -127,9 +112,11 @@ export function JobsList({bodyshop,}) { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - sorter: true, + sorter: (a, b) => + parseInt((a.ro_number || "0").replace(/\D/g, "")) - + parseInt((b.ro_number || "0").replace(/\D/g, "")), sortOrder: - sortcolumn === "ro_number" && sortorder, + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( {record.ro_number || t("general.labels.na")} {record.production_vars && record.production_vars.alert ? ( - + ) : null} {record.suspended && ( - + )} {record.iouparent && ( - + )} @@ -158,20 +145,22 @@ export function JobsList({bodyshop,}) { dataIndex: "owner", key: "owner", ellipsis: true, + responsive: ["md"], - sorter: false, - sortOrder: sortcolumn === "owner" && sortorder, + sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( e.stopPropagation()} > - + ) : ( - + ); }, @@ -183,7 +172,7 @@ export function JobsList({bodyshop,}) { ellipsis: true, responsive: ["md"], render: (text, record) => ( - + ), }, { @@ -193,7 +182,7 @@ export function JobsList({bodyshop,}) { ellipsis: true, responsive: ["md"], render: (text, record) => ( - + ), }, @@ -202,9 +191,10 @@ export function JobsList({bodyshop,}) { dataIndex: "status", key: "status", ellipsis: true, - sorter: true, + + sorter: (a, b) => alphaSort(a.status, b.status), sortOrder: - sortcolumn === "status" && sortorder, + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, filters: (jobs && jobs @@ -219,6 +209,7 @@ export function JobsList({bodyshop,}) { [], onFilter: (value, record) => value.includes(record.status), }, + { title: t("jobs.fields.vehicle"), dataIndex: "vehicle", @@ -246,10 +237,11 @@ export function JobsList({bodyshop,}) { dataIndex: "plate_no", key: "plate_no", ellipsis: true, + responsive: ["md"], - sorter: true, + sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), sortOrder: - sortcolumn === "plate_no" && sortorder, + state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, }, { title: t("jobs.fields.clm_no"), @@ -257,9 +249,9 @@ export function JobsList({bodyshop,}) { key: "clm_no", ellipsis: true, responsive: ["md"], - sorter: true, + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), sortOrder: - sortcolumn === "clm_no" && sortorder, + state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, render: (text, record) => `${record.clm_no || ""}${ record.po_number ? ` (PO: ${record.po_number})` : "" @@ -291,9 +283,10 @@ export function JobsList({bodyshop,}) { key: "clm_total", responsive: ["md"], ellipsis: true, - sorter: true, + + sorter: (a, b) => a.clm_total - b.clm_total, sortOrder: - sortcolumn === "clm_total" && sortorder, + state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, render: (text, record) => ( {record.clm_total} ), @@ -356,57 +349,26 @@ export function JobsList({bodyshop,}) { title={t("titles.bc.jobs-active")} extra={ - {search.search && ( - <> - - {t("general.labels.searchresults", { search: search.search })} - - - - )} { - search.search = value; - history.push({ search: queryString.stringify(search) }); - searchJobs(value); + placeholder={t("general.labels.search")} + onChange={(e) => { + setSearchText(e.target.value); }} - loading={loading || searchLoading} + value={searchText} enterButton /> } > { + onRow={(record, rowIndex) => { return { - onClick: () => { + onClick: (event) => { handleOnRowClick(record); }, }; @@ -430,4 +392,4 @@ export function JobsList({bodyshop,}) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(JobsList); +export default connect(mapStateToProps, null)(JobsList); diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js index 24a564872..a01dd54c2 100644 --- a/client/src/components/rbac-wrapper/rbac-defaults.js +++ b/client/src/components/rbac-wrapper/rbac-defaults.js @@ -55,10 +55,11 @@ const ret = { "shiftclock:view": 2, "shop:config": 4, - "shop:rbac": 5, - "shop:vendors": 2, "shop:dashboard": 3, + "shop:rbac": 5, + "shop:reportcenter": 2, "shop:templates": 4, + "shop:vendors": 2, "temporarydocs:view": 2, diff --git a/client/src/components/report-center-modal/report-center-modal.container.jsx b/client/src/components/report-center-modal/report-center-modal.container.jsx index f0d361785..84fe65560 100644 --- a/client/src/components/report-center-modal/report-center-modal.container.jsx +++ b/client/src/components/report-center-modal/report-center-modal.container.jsx @@ -5,6 +5,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; +import RbacWrapperComponent from "../rbac-wrapper/rbac-wrapper.component"; import ReportCenterModalComponent from "./report-center-modal.component"; const mapStateToProps = createStructuredSelector({ @@ -33,7 +34,9 @@ export function ReportCenterModalContainer({ destroyOnClose width="80%" > - + + + ); } diff --git a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx index 0117279d5..af4f28695 100644 --- a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx @@ -29,7 +29,7 @@ export default connect( export function ScoreboardTimeTicketsStats({ bodyshop }) { const { t } = useTranslation(); - const startDate = moment().startOf("month") + const startDate = moment().startOf("month"); const endDate = moment().endOf("month"); const fixedPeriods = useMemo(() => { @@ -84,6 +84,8 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -340,11 +342,21 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { larData.push({ ...r, ...lar }); }); + const jobData = {}; + data.jobs.forEach((job) => { + job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0); + }); + jobData.tthrs = data.jobs + .reduce((acc, val) => acc + val.tthrs, 0) + .toFixed(1); + jobData.count = data.jobs.length.toFixed(0); + return { fixed: ret, combinedData: combinedData, labData: labData, larData: larData, + jobData: jobData, }; }, [fixedPeriods, data, bodyshop]); @@ -356,7 +368,10 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { - + {/* This Month */} - + @@ -482,7 +482,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Last Month */} - + @@ -556,7 +556,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Efficiency Over Period */} - + + + + + + + + + + + + {t("scoreboard.labels.totalhrs")} + + } + value={jobData.tthrs} + valueStyle={{ + fontSize: statisticSize, + fontWeight: statisticWeight, + }} + /> + + + + {/* Disclaimer */} 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 fe4f80f31..e4152fb88 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -28,18 +28,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { return ( - - - + + + + + + + + + + + + @@ -173,26 +209,38 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { + + + @@ -208,30 +256,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - - - - - - - @@ -280,6 +292,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > + + + + + + + + + + + + + + + @@ -329,74 +401,14 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { - - - - - - - - - - - - - - - @@ -412,18 +424,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + - - - - - - @@ -556,18 +556,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + + + + + + + + + + - - - - - - - - - {Simple_Inventory.treatment === "on" && ( <> Config", "dashboard": "Shop -> Dashboard", "rbac": "Shop -> RBAC", + "reportcenter": "Shop -> Report Center", "templates": "Shop -> Templates", "vendors": "Shop -> Vendors" }, @@ -2695,6 +2697,7 @@ "efficiencyoverperiod": "Efficiency over Selected Dates", "entries": "Scoreboard Entries", "jobs": "Jobs", + "jobscompletednotinvoiced": "Completed Not Invoiced", "lastmonth": "Last Month", "lastweek": "Last Week", "monthlytarget": "Monthly", @@ -2709,6 +2712,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", "weeklytarget": "Weekly", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 33cf621da..8afe8075e 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -203,6 +203,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -447,6 +448,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -2695,6 +2697,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2712,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 3b5251fa7..36a5fa713 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -203,6 +203,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -447,6 +448,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -2695,6 +2697,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2709,6 +2712,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 7221649ec..e203bcbf3 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -50,7 +50,7 @@ exports.createUser = async (req, res) => { `, { user: { - email, + email: email.toLowerCase(), authid: userRecord.uid, associations: { data: [{ shopid, authlevel, active: true }],