diff --git a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx index fcdcca0b6..260589c2f 100644 --- a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx +++ b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx @@ -15,6 +15,7 @@ import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -210,7 +211,7 @@ export function AccountingPayablesTableComponent({ toggleChatVisible()} style={{ cursor: "pointer" }} > - + {t("messaging.labels.messaging")} )} diff --git a/client/src/components/chat-popup/chat-popup.styles.scss b/client/src/components/chat-popup/chat-popup.styles.scss index 398762700..be9a5c0a7 100644 --- a/client/src/components/chat-popup/chat-popup.styles.scss +++ b/client/src/components/chat-popup/chat-popup.styles.scss @@ -13,6 +13,9 @@ height: 100%; } } +.chat-popup-info-icon { + margin-right: 5px; +} @media only screen and (min-width: 992px) { .chat-popup { diff --git a/client/src/components/contract-jobs/contract-jobs.component.jsx b/client/src/components/contract-jobs/contract-jobs.component.jsx index 10abd5b22..d84dd8e2a 100644 --- a/client/src/components/contract-jobs/contract-jobs.component.jsx +++ b/client/src/components/contract-jobs/contract-jobs.component.jsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { alphaSort } from "../../utils/sorters"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import {pageLimit} from "../../utils/config"; export default function ContractsJobsComponent({ loading, @@ -175,7 +176,7 @@ export default function ContractsJobsComponent({ loading={loading} pagination={{ position: "top", - defaultPageSize: 10, + defaultPageSize: pageLimit, defaultCurrent: defaultCurrent, }} columns={columns} diff --git a/client/src/components/contracts-list/contracts-list.component.jsx b/client/src/components/contracts-list/contracts-list.component.jsx index fa9439c65..cd25e1e9a 100644 --- a/client/src/components/contracts-list/contracts-list.component.jsx +++ b/client/src/components/contracts-list/contracts-list.component.jsx @@ -13,6 +13,7 @@ import moment from "moment"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -209,7 +210,7 @@ export function ContractsList({ }} pagination={{ position: "top", - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: total, }} diff --git a/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx b/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx index 705337f21..125033101 100644 --- a/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx +++ b/client/src/components/courtesy-car-contract-list/courtesy-car-contract-list.component.jsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { Link, useHistory, useLocation } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; import { alphaSort } from "../../utils/sorters"; +import {pageLimit} from "../../utils/config"; export default function CourtesyCarContractListComponent({ contracts, @@ -89,7 +90,7 @@ export default function CourtesyCarContractListComponent({ scroll={{ x: true }} pagination={{ position: "top", - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: totalContracts, }} diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index e512cca20..f73e35512 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -7,6 +7,7 @@ import { Link, useHistory, useLocation } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; import { alphaSort } from "../../utils/sorters"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import {pageLimit} from "../../utils/config"; export default function CsiResponseListPaginated({ refetch, @@ -106,7 +107,7 @@ export default function CsiResponseListPaginated({ loading={loading} pagination={{ position: "top", - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: total, }} diff --git a/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx b/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx index dff539d19..c3a69a708 100644 --- a/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx +++ b/client/src/components/dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx @@ -6,6 +6,7 @@ import { alphaSort } from "../../../utils/sorters"; import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component"; import Dinero from "dinero.js"; import DashboardRefreshRequired from "../refresh-required.component"; +import {pageLimit} from "../../../utils/config"; export default function DashboardMonthlyJobCosting({ data, ...cardProps }) { const { t } = useTranslation(); @@ -118,7 +119,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
`${record.InvoiceNumber}${record.Account}`} dataSource={allocationsSummary} diff --git a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx index d244a341c..7c9944149 100644 --- a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx +++ b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx @@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import Dinero from "dinero.js"; import { SyncOutlined } from "@ant-design/icons"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -94,7 +95,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) { )}
), }, - { title: t("jobs.fields.owner"), dataIndex: "ownr_ln", @@ -125,7 +125,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("vehicles.fields.plate_no"), dataIndex: "plate_no", key: "plate_no", - ellipsis: true, sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no), sortOrder: sortcolumn === "plate_no" && sortorder, @@ -137,7 +136,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.clm_no"), dataIndex: "clm_no", key: "clm_no", - ellipsis: true, sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no), sortOrder: sortcolumn === "clm_no" && sortorder, @@ -259,11 +257,11 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { pagination={ search?.search ? { - pageSize: 25, + pageSize: pageLimit, showSizeChanger: false, } : { - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: total, showSizeChanger: false, diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index e28e21901..f025f1401 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -1,395 +1,433 @@ -import { - SyncOutlined, - ExclamationCircleFilled, - PauseCircleOutlined, - BranchesOutlined, -} from "@ant-design/icons"; -import { useQuery } from "@apollo/client"; -import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd"; +import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOutlined,} from "@ant-design/icons"; +import {useQuery} from "@apollo/client"; +import {Button, Card, Grid, Input, Space, Table, Tooltip, Typography} from "antd"; import queryString from "query-string"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { Link, useHistory, useLocation } from "react-router-dom"; -import { createStructuredSelector } from "reselect"; -import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -import { onlyUnique } from "../../utils/arrayHelper"; +import React, {useEffect, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {connect} from "react-redux"; +import {Link, useHistory, useLocation} from "react-router-dom"; +import {createStructuredSelector} from "reselect"; +import {QUERY_ALL_ACTIVE_JOBS_PAGINATED} from "../../graphql/jobs.queries"; +import {selectBodyshop} from "../../redux/user/user.selectors"; +import {onlyUnique} from "../../utils/arrayHelper"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { alphaSort } from "../../utils/sorters"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import {flattenDeep} from "lodash"; +import { pageLimit } from '../../utils/config'; +import axios from "axios"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop, }); -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, { - variables: { - statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); +const mapDispatchToProps = () => ({ +}); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: { text: "" }, - }); +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; - const { t } = useTranslation(); - const history = useHistory(); - const [searchText, setSearchText] = useState(""); + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; - if (error) return ; + const {loading, error, data, refetch} = useQuery(QUERY_ALL_ACTIVE_JOBS_PAGINATED, { + variables: { + offset: page ? (page - 1) * pageLimit : 0, + limit: 10, + 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 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 total = data?.jobs_aggregate?.aggregate?.count || 0; - const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); - }; + const [state, setState] = useState({ + filteredInfo: {text: ""}, + }); - const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - history.push({ - search: queryString.stringify({ - ...searchParams, - selected: record.id, - }), - }); - } + const {t} = useTranslation(); + const history = useHistory(); + + const jobs = data?.jobs || []; + + 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)}); + }; + + 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 columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => - parseInt((a.ro_number || "0").replace(/\D/g, "")) - - parseInt((b.ro_number || "0").replace(/\D/g, "")), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + history.push({ + search: queryString.stringify({ + ...search, + selected: record.id, + }), + }); + } + } + }; - render: (text, record) => ( - e.stopPropagation()} - > - - {record.ro_number || t("general.labels.na")} - {record.production_vars && record.production_vars.alert ? ( - - ) : null} - {record.suspended && ( - - )} - {record.iouparent && ( - - - - )} - - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: true, + sortOrder: + sortcolumn === "ro_number" && sortorder, - responsive: ["md"], - 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()} - > - - - ) : ( - - + render: (text, record) => ( + e.stopPropagation()} + > + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + responsive: ["md"], + sorter: false, + sortOrder: sortcolumn === "owner" && sortorder, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + ) : ( + + - ); - }, - }, - { - title: t("jobs.fields.ownr_ph1"), - dataIndex: "ownr_ph1", - key: "ownr_ph1", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - ellipsis: true, - - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - filters: - (jobs && - jobs - .map((j) => j.status) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Status*", - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.status), - }, - - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - e.stopPropagation()} - > - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("vehicles.fields.plate_no"), - dataIndex: "plate_no", - key: "plate_no", - ellipsis: true, - - responsive: ["md"], - sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), - sortOrder: - state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, - }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - responsive: ["md"], - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: - state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, - render: (text, record) => - `${record.clm_no || ""}${ - record.po_number ? ` (PO: ${record.po_number})` : "" - }`, - }, - { - title: t("jobs.fields.ins_co_nm"), - dataIndex: "ins_co_nm", - key: "ins_co_nm", - ellipsis: true, - filters: - (jobs && - jobs - .map((j) => j.ins_co_nm) - .filter(onlyUnique) - .map((s) => { - return { - text: s, - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.ins_co_nm), - responsive: ["md"], - }, - { - title: t("jobs.fields.clm_total"), - dataIndex: "clm_total", - key: "clm_total", - responsive: ["md"], - ellipsis: true, - - sorter: (a, b) => a.clm_total - b.clm_total, - sortOrder: - state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, - render: (text, record) => ( - {record.clm_total} - ), - }, - { - title: t("jobs.labels.estimator"), - dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", - ellipsis: true, - responsive: ["xl"], - filterSearch: true, - filters: - (jobs && - jobs - .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "N/A", - value: [s], - }; - })) || - [], - onFilter: (value, record) => - value.includes( - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() - ), - render: (text, record) => - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), - }, - { - title: t("jobs.fields.comment"), - dataIndex: "comment", - key: "comment", - ellipsis: true, - responsive: ["md"], - }, - // { - // title: t("jobs.fields.owner_owing"), - // dataIndex: "owner_owing", - // key: "owner_owing", - // responsive: ["md"], - // render: (text, record) => ( - // {record.owner_owing} - // ), - // }, - ]; - - const scrollMapper = { - xs: true, - sm: true, - md: true, - lg: "100%", - xl: "100%", - xxl: "100%", - }; - - return ( - - - { - setSearchText(e.target.value); - }} - value={searchText} - enterButton - /> - - } - > -
{ - handleOnRowClick(record); - }, - selectedRowKeys: [selected], - type: "radio", - }} - onChange={handleTableChange} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); + ); }, - }; - }} - /> - - ); + }, + { + title: t("jobs.fields.ownr_ph1"), + dataIndex: "ownr_ph1", + key: "ownr_ph1", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + { + title: t("jobs.fields.ownr_ph2"), + dataIndex: "ownr_ph2", + key: "ownr_ph2", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: true, + sortOrder: + sortcolumn === "status" && sortorder, + filters: + (jobs && + jobs + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + })) || + [], + onFilter: (value, record) => value.includes(record.status), + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("vehicles.fields.plate_no"), + dataIndex: "plate_no", + key: "plate_no", + ellipsis: true, + responsive: ["md"], + sorter: true, + sortOrder: + sortcolumn === "plate_no" && sortorder, + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + ellipsis: true, + responsive: ["md"], + sorter: true, + sortOrder: + sortcolumn === "clm_no" && sortorder, + render: (text, record) => + `${record.clm_no || ""}${ + record.po_number ? ` (PO: ${record.po_number})` : "" + }`, + }, + { + title: t("jobs.fields.ins_co_nm"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + filters: + (jobs && + jobs + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s, + value: [s], + }; + })) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), + responsive: ["md"], + }, + { + title: t("jobs.fields.clm_total"), + dataIndex: "clm_total", + key: "clm_total", + responsive: ["md"], + ellipsis: true, + sorter: true, + sortOrder: + sortcolumn === "clm_total" && sortorder, + render: (text, record) => ( + {record.clm_total} + ), + }, + { + title: t("jobs.labels.estimator"), + dataIndex: "jobs.labels.estimator", + key: "jobs.labels.estimator", + ellipsis: true, + responsive: ["xl"], + filterSearch: true, + filters: + (jobs && + jobs + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "N/A", + value: [s], + }; + })) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + responsive: ["md"], + }, + // { + // title: t("jobs.fields.owner_owing"), + // dataIndex: "owner_owing", + // key: "owner_owing", + // responsive: ["md"], + // render: (text, record) => ( + // {record.owner_owing} + // ), + // }, + ]; + + const scrollMapper = { + xs: true, + sm: true, + md: true, + lg: "100%", + xl: "100%", + xxl: "100%", + }; + + return ( + + {search.search && ( + <> + + {t("general.labels.searchresults", { search: search.search })} + + + + )} + + { + search.search = value; + history.push({ search: queryString.stringify(search) }); + searchJobs(value); + }} + loading={loading || searchLoading} + enterButton + /> + + } + > +
{ + handleOnRowClick(record); + }, + selectedRowKeys: [selected], + type: "radio", + }} + onChange={handleTableChange} + onRow={(record) => { + return { + onClick: () => { + handleOnRowClick(record); + }, + }; + }} + /> + + ); } -export default connect(mapStateToProps, null)(JobsList); +export default connect(mapStateToProps, mapDispatchToProps)(JobsList); diff --git a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx index 678c5e228..fee461df2 100644 --- a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx +++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx @@ -20,6 +20,7 @@ import { alphaSort } from "../../utils/sorters"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -377,7 +378,7 @@ export function JobsReadyList({ bodyshop }) { >
({ setPartsOrderContext: (context) => @@ -295,11 +296,11 @@ export function BillsListPage({ pagination={ search?.search ? { - pageSize: 25, + pageSize: pageLimit, showSizeChanger: false, } : { - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: total, showSizeChanger: false, diff --git a/client/src/pages/export-logs/export-logs.page.component.jsx b/client/src/pages/export-logs/export-logs.page.component.jsx index 0b4dcd634..0c6517a58 100644 --- a/client/src/pages/export-logs/export-logs.page.component.jsx +++ b/client/src/pages/export-logs/export-logs.page.component.jsx @@ -12,6 +12,7 @@ import AlertComponent from "../../components/alert/alert.component"; import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateTimeFormatter } from "../../utils/DateFormatter"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -178,7 +179,7 @@ export function ExportLogsPageComponent({ bodyshop }) { loading={loading} pagination={{ position: "top", - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: data && data.search_exportlog_aggregate.aggregate.count, }} diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index 2518d6c16..a678ecab9 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -33,7 +33,7 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - offset: page ? (page - 1) * 25 : 0, + offset: page ? (page - 1) * 10 : 0, limit: 25, ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ 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 5d6226bdb..62970d788 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.component.jsx @@ -18,6 +18,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; import { alphaSort, dateSort } from "../../utils/sorters"; import useLocalStorage from "../../utils/useLocalStorage"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -296,7 +297,7 @@ export function PartsQueuePageComponent({ bodyshop }) { loading={loading} pagination={{ position: "top", - pageSize: 50, + pageSize: pageLimit, // current: parseInt(page || 1), // total: data && data.jobs_aggregate.aggregate.count, }} diff --git a/client/src/pages/phonebook/phonebook.page.component.jsx b/client/src/pages/phonebook/phonebook.page.component.jsx index 9a7a13777..baf4e18ec 100644 --- a/client/src/pages/phonebook/phonebook.page.component.jsx +++ b/client/src/pages/phonebook/phonebook.page.component.jsx @@ -17,6 +17,7 @@ import { import ChatOpenButton from "../../components/chat-open-button/chat-open-button.component"; import { alphaSort } from "../../utils/sorters"; import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component"; +import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -189,7 +190,7 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) { loading={loading} pagination={{ position: "top", - pageSize: 25, + pageSize: pageLimit, current: parseInt(page || 1), total: data && data.search_phonebook_aggregate.aggregate.count, }} diff --git a/client/src/utils/config.js b/client/src/utils/config.js new file mode 100644 index 000000000..2a968907c --- /dev/null +++ b/client/src/utils/config.js @@ -0,0 +1,4 @@ + +// Sometimes referred to as PageSize, this variable controls the amount of records +// to show on one page during pagination. +export const pageLimit = 50; diff --git a/libs/awsUtils.js b/libs/awsUtils.js new file mode 100644 index 000000000..4f7ee9aa4 --- /dev/null +++ b/libs/awsUtils.js @@ -0,0 +1,56 @@ +require("dotenv").config({ + path: require("path").resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +const {isNil} = require('lodash'); +const aws4 = require("aws4"); +const {Connection, Client} = require("@opensearch-project/opensearch"); +const {defaultProvider} = require("@aws-sdk/credential-provider-node"); + +const createAwsConnector = (credentials, region) => { + class AmazonConnection extends Connection { + buildRequestObject(params) { + const request = super.buildRequestObject(params); + request.service = "es"; + request.region = region; + request.headers = request.headers || {}; + request.headers["host"] = request.hostname; + + return aws4.sign(request, credentials); + } + } + return { + Connection: AmazonConnection, + }; +}; + +const getClient = async () => { + + // We have manual configuration for OpenSearch, + // Return a client using these custom credentials + if ( + !isNil(process.env.OPEN_SEARCH_PASSWORD) && + !isNil(process.env.OPEN_SEARCH_USER) && + !isNil(process.env.OPEN_SEARCH_HOST) && + !isNil(process.env.OPEN_SEARCH_PROTOCOL) + ) { + // The URI is currently being stored in its entirety, so strip protocol prior to rebuilding it. + const hostUrl = process.env.OPEN_SEARCH_HOST.replace(/^https?:\/\//i, ''); + const node = `${process.env.OPEN_SEARCH_PROTOCOL}://${process.env.OPEN_SEARCH_USER}:${process.env.OPEN_SEARCH_PASSWORD}@${hostUrl}`; + + return new Client({ + node, + }); + } + + // Default to the AWS Credentials Provider. + const credentials = await defaultProvider()(); + return new Client({ + ...createAwsConnector(credentials, "ca-central-1"), + node: process.env.OPEN_SEARCH_HOST, + }); +}; + +module.exports = { getClient }; \ No newline at end of file diff --git a/os-loader.js b/os-loader.js index a7876187a..bdabb87ac 100644 --- a/os-loader.js +++ b/os-loader.js @@ -1,59 +1,17 @@ -const Dinero = require("dinero.js"); - -//const client = require("../graphql-client/graphql-client").client; -const _ = require("lodash"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("./server/utils/logger"); - -const path = require("path"); -const client = require("./server/graphql-client/graphql-client").client; require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` + path: require("path").resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` ), }); -const { Client, Connection } = require("@opensearch-project/opensearch"); -const { defaultProvider } = require("@aws-sdk/credential-provider-node"); -const aws4 = require("aws4"); -const { gql } = require("graphql-request"); -const gqlclient = require("./server/graphql-client/graphql-client").client; -// const osClient = new Client({ -// node: `https://imex:Wl0d8k@!@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com/`, -// }); -var host = process.env.OPEN_SEARCH_HOST; // e.g. https://my-domain.region.es.amazonaws.com -const createAwsConnector = (credentials, region) => { - class AmazonConnection extends Connection { - buildRequestObject(params) { - const request = super.buildRequestObject(params); - request.service = "es"; - request.region = region; - request.headers = request.headers || {}; - request.headers["host"] = request.hostname; - - return aws4.sign(request, credentials); - } - } - return { - Connection: AmazonConnection, - }; -}; - -const getClient = async () => { - const credentials = await defaultProvider()(); - return new Client({ - ...createAwsConnector(credentials, "ca-central-1"), - node: host, - }); -}; +const {omit} = require("lodash"); +const gqlClient = require("./server/graphql-client/graphql-client").client; +const getClient = require('./libs/awsUtils'); async function OpenSearchUpdateHandler(req, res) { try { - var osClient = await getClient(); - // const osClient = new Client({ - // node: `https://imex:password@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com`, - // }); + const osClient = await getClient(); //Clear out all current documents // const deleteResult = await osClient.deleteByQuery({ @@ -67,11 +25,11 @@ async function OpenSearchUpdateHandler(req, res) { // return; - var batchSize = 1000; - var promiseQueue = []; + const batchSize = 1000; + const promiseQueue = []; //Jobs Load. - const jobsData = await gqlclient.request(`query{jobs{ + const jobsData = await gqlClient.request(`query{jobs{ id bodyshopid:shopid clm_no @@ -105,7 +63,7 @@ async function OpenSearchUpdateHandler(req, res) { } //Owner Load - const ownersData = await gqlclient.request(`{ + const ownersData = await gqlClient.request(`{ owners { id bodyshopid: shopid @@ -131,7 +89,7 @@ async function OpenSearchUpdateHandler(req, res) { } //Vehicles - const vehiclesData = await gqlclient.request(`{ + const vehiclesData = await gqlClient.request(`{ vehicles { id bodyshopid: shopid @@ -158,7 +116,7 @@ async function OpenSearchUpdateHandler(req, res) { } //payments - const paymentsData = await gqlclient.request(`{ + const paymentsData = await gqlClient.request(`{ payments { id amount @@ -198,7 +156,7 @@ async function OpenSearchUpdateHandler(req, res) { slicedArray.forEach((payment) => { bulkOperation.push({ index: { _index: "payments", _id: payment.id } }); bulkOperation.push({ - ..._.omit(payment, ["job"]), + ...omit(payment, ["job"]), bodyshopid: payment.job.bodyshopid, }); }); @@ -206,7 +164,7 @@ async function OpenSearchUpdateHandler(req, res) { } //bills - const billsData = await gqlclient.request(`{ + const billsData = await gqlClient.request(`{ bills { id date @@ -235,7 +193,7 @@ async function OpenSearchUpdateHandler(req, res) { slicedArray.forEach((bill) => { bulkOperation.push({ index: { _index: "bills", _id: bill.id } }); bulkOperation.push({ - ..._.omit(bill, ["job"]), + ...omit(bill, ["job"]), bodyshopid: bill.job.bodyshopid, }); }); diff --git a/server/opensearch/os-handler.js b/server/opensearch/os-handler.js index b6e5fc490..7cb544400 100644 --- a/server/opensearch/os-handler.js +++ b/server/opensearch/os-handler.js @@ -1,49 +1,18 @@ -const queries = require("../graphql-client/queries"); -const {pick} = require("lodash"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); -//const client = require("../graphql-client/graphql-client").client; - -const path = require("path"); -const client = require("../graphql-client/graphql-client").client; require("dotenv").config({ - path: path.resolve( + path: require("path").resolve( process.cwd(), `.env.${process.env.NODE_ENV || "development"}` ), }); -const {Client, Connection} = require("@opensearch-project/opensearch"); -const {defaultProvider} = require("@aws-sdk/credential-provider-node"); -const aws4 = require("aws4"); -const {gql} = require("graphql-request"); -var host = process.env.OPEN_SEARCH_HOST; +const GraphQLClient = require("graphql-request").GraphQLClient; +//const client = require("../graphql-client/graphql-client").client; +const logger = require("../utils/logger"); +const queries = require("../graphql-client/queries"); +const client = require("../graphql-client/graphql-client").client; +const {pick, isNil} = require("lodash"); +const {getClient} = require('../../libs/awsUtils'); -const createAwsConnector = (credentials, region) => { - class AmazonConnection extends Connection { - buildRequestObject(params) { - const request = super.buildRequestObject(params); - request.service = "es"; - request.region = region; - request.headers = request.headers || {}; - request.headers["host"] = request.hostname; - - return aws4.sign(request, credentials); - } - } - - return { - Connection: AmazonConnection, - }; -}; - -const getClient = async () => { - const credentials = await defaultProvider()(); - return new Client({ - ...createAwsConnector(credentials, "ca-central-1"), - node: host, - }); -}; async function OpenSearchUpdateHandler(req, res) { if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { @@ -51,10 +20,8 @@ async function OpenSearchUpdateHandler(req, res) { return; } try { - var osClient = await getClient(); - // const osClient = new Client({ - // node: `https://imex:@search-imexonline-search-ixp2stfvwp6qocjsowzjzyreoy.ca-central-1.es.amazonaws.com/`, - // }); + + const osClient = await getClient(); if (req.body.event.op === "DELETE") { let response; @@ -197,14 +164,12 @@ async function OpenSearchUpdateHandler(req, res) { body: document, }; - let response; - response = await osClient.index(payload); + const response = await osClient.index(payload); console.log(response.body); res.status(200).json(response.body); } } catch (error) { res.status(400).json(JSON.stringify(error)); - } finally { } } @@ -240,6 +205,8 @@ async function OpenSearchSearchHandler(req, res) { const osClient = await getClient(); + const bodyShopIdMatchOverride = isNil(process.env.BODY_SHOP_ID_MATCH_OVERRIDE) ? assocs.associations[0].shopid : process.env.BODY_SHOP_ID_MATCH_OVERRIDE + const {body} = await osClient.search({ ...(index ? {index} : {}), body: { @@ -249,7 +216,7 @@ async function OpenSearchSearchHandler(req, res) { must: [ { match: { - bodyshopid: assocs.associations[0].shopid, + bodyshopid: bodyShopIdMatchOverride, }, }, { @@ -318,7 +285,6 @@ async function OpenSearchSearchHandler(req, res) { error: JSON.stringify(error), }); res.status(400).json(error); - } finally { } }