diff --git a/client/src/components/global-search/global-search-os.component.jsx b/client/src/components/global-search/global-search-os.component.jsx new file mode 100644 index 000000000..8f1cb596a --- /dev/null +++ b/client/src/components/global-search/global-search-os.component.jsx @@ -0,0 +1,216 @@ +import { AutoComplete, Divider, Input, Space } from "antd"; +import axios from "axios"; +import _ from "lodash"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useHistory } from "react-router-dom"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; +import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; + +export default function GlobalSearchOs() { + const { t } = useTranslation(); + const history = useHistory(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(false); + + const executeSearch = async (v) => { + if (v && v && v !== "" && v.length >= 3) { + try { + setLoading(true); + const searchData = await axios.post("/search", { + search: v, + }); + + const resultsByType = { + payments: [], + jobs: [], + bills: [], + owners: [], + vehicles: [], + }; + + searchData.data.hits.hits.forEach((hit) => { + resultsByType[hit._index].push(hit._source); + }); + setData([ + { + label: renderTitle(t("menus.header.search.jobs")), + options: resultsByType.jobs.map((job) => { + return { + key: job.id, + value: job.ro_number, + label: ( + + }> + {job.ro_number || t("general.labels.na")} + {`${job.status || ""}`} + + + + {`${job.v_model_yr || ""} ${ + job.v_make_desc || "" + } ${job.v_model_desc || ""}`} + {`${job.clm_no || ""}`} + + + ), + }; + }), + }, + { + label: renderTitle(t("menus.header.search.owners")), + options: resultsByType.owners.map((owner) => { + return { + key: owner.id, + value: OwnerNameDisplayFunction(owner), + label: ( + + } + wrap + > + + + + + {owner.ownr_ph1} + + + {owner.ownr_ph2} + + + + ), + }; + }), + }, + { + label: renderTitle(t("menus.header.search.vehicles")), + options: resultsByType.vehicles.map((vehicle) => { + return { + key: vehicle.id, + value: `${vehicle.v_model_yr || ""} ${ + vehicle.v_make_desc || "" + } ${vehicle.v_model_desc || ""}`, + label: ( + + }> + + {`${vehicle.v_model_yr || ""} ${ + vehicle.v_make_desc || "" + } ${vehicle.v_model_desc || ""}`} + + {vehicle.plate_no || ""} + + + {vehicle.v_vin || ""} + + + + + ), + }; + }), + }, + { + label: renderTitle(t("menus.header.search.payments")), + options: resultsByType.payments.map((payment) => { + return { + key: payment.id, + value: `${payment.job?.ro_number} ${payment.amount}`, + label: ( + + }> + {payment.paymentnum} + {payment.job?.ro_number} + {payment.memo || ""} + {payment.amount || ""} + {payment.transactionid || ""} + + + ), + }; + }), + }, + { + label: renderTitle(t("menus.header.search.bills")), + options: resultsByType.bills.map((bill) => { + return { + key: bill.id, + value: `${bill.invoice_number} - ${bill.vendor.name}`, + label: ( + + }> + {bill.invoice_number} + {bill.vendor.name} + {bill.date} + + + ), + }; + }), + }, + // { + // label: renderTitle(t("menus.header.search.phonebook")), + // options: resultsByType.search_phonebook.map((pb) => { + // return { + // key: pb.id, + // value: `${pb.firstname || ""} ${pb.lastname || ""} ${ + // pb.company || "" + // }`, + // label: ( + // + // }> + // {`${pb.firstname || ""} ${pb.lastname || ""} ${ + // pb.company || "" + // }`} + // {pb.phone1} + // {pb.email} + // + // + // ), + // }; + // }), + // }, + ]); + } catch (error) { + console.log("Error while fetching search results", error); + } finally { + setLoading(false); + } + } + }; + const debouncedExecuteSearch = _.debounce(executeSearch, 750); + + const handleSearch = (value) => { + console.log("Handle Search"); + debouncedExecuteSearch(value); + }; + + const renderTitle = (title) => { + return {title}; + }; + + return ( + { + history.push(opt.label.props.to); + }} + > + + + ); +} diff --git a/client/src/pages/jobs-all/jobs-all.container.jsx b/client/src/pages/jobs-all/jobs-all.container.jsx index 539271e7d..a36745a0e 100644 --- a/client/src/pages/jobs-all/jobs-all.container.jsx +++ b/client/src/pages/jobs-all/jobs-all.container.jsx @@ -39,7 +39,7 @@ export function AllJobs({ setBreadcrumbs, setSelectedHeader }) { ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}), order: [ { - [sortcolumn || "created_at"]: + [sortcolumn || "ro_number"]: sortorder && sortorder !== "false" ? sortorder === "descend" ? "desc" diff --git a/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/down.sql b/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/down.sql new file mode 100644 index 000000000..9a9ec807f --- /dev/null +++ b/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."jobs_idx_status_hash"; diff --git a/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/up.sql b/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/up.sql new file mode 100644 index 000000000..1343f6695 --- /dev/null +++ b/hasura/migrations/1682522234404_create_index_jobs_idx_status_hash/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "jobs_idx_status_hash" on + "public"."jobs" using hash ("status"); diff --git a/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/down.sql b/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/down.sql new file mode 100644 index 000000000..63b6b51e7 --- /dev/null +++ b/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_ronumber_btree"; diff --git a/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/up.sql b/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/up.sql new file mode 100644 index 000000000..0d93a227c --- /dev/null +++ b/hasura/migrations/1682522667065_create_index_idx_jobs_ronumber_btree/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_ronumber_btree" on + "public"."jobs" using btree ("ro_number"); diff --git a/os-loader.js b/os-loader.js new file mode 100644 index 000000000..d5a2c52a1 --- /dev/null +++ b/os-loader.js @@ -0,0 +1,237 @@ +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"}` + ), +}); +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, + }); +}; + +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`, + // }); + + //Clear out all current documents + // const deleteResult = await osClient.deleteByQuery({ + // index: ["*"], // ["jobs", "payments", "bills", "vehicles", "owners"], + // body: { + // query: { + // match_all: {}, + // }, + // }, + // }); + + // return; + + var batchSize = 1000; + var promiseQueue = []; + + //Jobs Load. + const jobsData = await gqlclient.request(`query{jobs{ + id + bodyshopid:shopid + ro_number + clm_no + ownr_fn + ownr_ln + status + ownr_co_nm + v_model_yr + v_make_desc + v_model_desc + }}`); + for (let i = 0; i <= jobsData.jobs.length / batchSize; i++) { + const slicedArray = jobsData.jobs.slice( + i * batchSize, + i * batchSize + batchSize + ); + const bulkOperation = []; + slicedArray.forEach((job) => { + bulkOperation.push({ index: { _index: "jobs", _id: job.id } }); + bulkOperation.push(job); + }); + promiseQueue.push(bulkOperation); + } + + //Owner Load + const ownersData = await gqlclient.request(`{ + owners { + id + bodyshopid: shopid + ownr_fn + ownr_ln + ownr_co_nm + ownr_ph1 + ownr_ph2 + } + } + `); + for (let i = 0; i <= ownersData.owners.length / batchSize; i++) { + const slicedArray = ownersData.owners.slice( + i * batchSize, + i * batchSize + batchSize + ); + const bulkOperation = []; + slicedArray.forEach((owner) => { + bulkOperation.push({ index: { _index: "owners", _id: owner.id } }); + bulkOperation.push(owner); + }); + promiseQueue.push(bulkOperation); + } + + //Vehicles + const vehiclesData = await gqlclient.request(`{ + vehicles { + id + bodyshopid: shopid +v_model_yr +v_model_desc +v_make_desc +v_color +v_vin +plate_no + } + } + `); + for (let i = 0; i <= vehiclesData.vehicles.length / batchSize; i++) { + const slicedArray = vehiclesData.vehicles.slice( + i * batchSize, + i * batchSize + batchSize + ); + const bulkOperation = []; + slicedArray.forEach((vehicle) => { + bulkOperation.push({ index: { _index: "vehicles", _id: vehicle.id } }); + bulkOperation.push(vehicle); + }); + promiseQueue.push(bulkOperation); + } + + //payments + const paymentsData = await gqlclient.request(`{ + payments { + id + amount + paymentnum + memo + transactionid + job { + id + ro_number + bodyshopid: shopid + } + } + } + + `); + for (let i = 0; i <= paymentsData.payments.length / batchSize; i++) { + const slicedArray = paymentsData.payments.slice( + i * batchSize, + i * batchSize + batchSize + ); + const bulkOperation = []; + slicedArray.forEach((payment) => { + bulkOperation.push({ index: { _index: "payments", _id: payment.id } }); + bulkOperation.push({ + ..._.omit(payment, ["job"]), + bodyshopid: payment.job.id, + }); + }); + promiseQueue.push(bulkOperation); + } + + //bills + const billsData = await gqlclient.request(`{ + bills { + id + total + invoice_number + date + vendor { + name + id + } + job { + ro_number + id + bodyshopid: shopid + } + } + } + + `); + for (let i = 0; i <= billsData.bills.length / batchSize; i++) { + const slicedArray = billsData.bills.slice( + i * batchSize, + i * batchSize + batchSize + ); + const bulkOperation = []; + slicedArray.forEach((bill) => { + bulkOperation.push({ index: { _index: "bills", _id: bill.id } }); + bulkOperation.push({ + ..._.omit(bill, ["job"]), + bodyshopid: bill.job.id, + }); + }); + promiseQueue.push(bulkOperation); + } + + //Load the entire queue. + for (const queueItem of promiseQueue) { + const insertJobsBulk = await osClient.bulk({ body: queueItem }); + + console.log( + ` ${insertJobsBulk.body.items.length} Records inserted in ${insertJobsBulk.body.took}.` + ); + if (insertJobsBulk.body.errors) + console.error("*** Error while inserting."); + } + } catch (error) { + console.log(error); + } +} + +OpenSearchUpdateHandler();