Compare commits
6 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a4679f86c | ||
|
|
a4dbc5250e | ||
|
|
a1d0e2df93 | ||
|
|
9a86a337bb | ||
|
|
7688f22161 | ||
|
|
efdcd06921 |
@@ -134,96 +134,11 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
const details = (() => {
|
||||
const original = data?.bills_by_pk ?? {};
|
||||
const updated = { ...bill, billlines };
|
||||
|
||||
const fmtVal = (key, val) =>
|
||||
val == null || val === ""
|
||||
? "<<empty>>"
|
||||
: typeof val === "number" && key.toLowerCase().includes("price")
|
||||
? `$${val.toFixed(2)}`
|
||||
: val;
|
||||
|
||||
const keysToTrack = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"];
|
||||
const lineVals = (obj) => keysToTrack.map((k) => fmtVal(k, obj[k])).join(", ");
|
||||
|
||||
const changed = Object.entries(updated)
|
||||
.filter(([k, v]) => v != null && v !== "" && k !== "billlines" && k !== "__typename")
|
||||
.map(([k, v]) => {
|
||||
const orig = original[k];
|
||||
if (k === "date") {
|
||||
const a = orig ? dayjs(orig) : null;
|
||||
const b = v ? (dayjs.isDayjs(v) ? v : dayjs(v)) : null;
|
||||
return (a && b && a.isSame(b, "day")) || (!a && !b)
|
||||
? null
|
||||
: `date: ${a?.format("YYYY-MM-DD") ?? "<<empty>>"} → ${b?.format("YYYY-MM-DD") ?? "<<empty>>"}`;
|
||||
}
|
||||
return typeof orig === "object" || typeof v === "object" || String(orig) === String(v)
|
||||
? null
|
||||
: `${k}: ${fmtVal(k, orig)} → ${fmtVal(k, v)}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const origLines = original.billlines ?? [];
|
||||
const updLines = updated.billlines ?? [];
|
||||
|
||||
const addedObjs = updLines
|
||||
.filter((l) => !l.id)
|
||||
.map((u) => ({ label: u.line_desc || u.description || "new line", vals: lineVals(u), handled: false }));
|
||||
|
||||
const removedObjs = origLines
|
||||
.filter((o) => !updLines.some((u) => u.id === o.id))
|
||||
.map((o) => ({
|
||||
label: o.line_desc || o.description || o.id || "removed line",
|
||||
vals: lineVals(o),
|
||||
handled: false
|
||||
}));
|
||||
|
||||
const labelToAdded = addedObjs.reduce((m, a) => m.set(a.label, [...(m.get(a.label) ?? []), a]), new Map());
|
||||
|
||||
const modified = [
|
||||
...removedObjs.reduce((acc, r) => {
|
||||
const candidates = labelToAdded.get(r.label) ?? [];
|
||||
const exact = candidates.find((c) => c.vals === r.vals && !c.handled);
|
||||
if (exact) {
|
||||
exact.handled = r.handled = true;
|
||||
return acc;
|
||||
} // identical → cancel out
|
||||
const diff = candidates.find((c) => c.vals !== r.vals && !c.handled);
|
||||
if (diff) {
|
||||
diff.handled = r.handled = true;
|
||||
acc.push(`${r.label}: ${r.vals} → ${diff.vals}`);
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
...updLines
|
||||
.filter((u) => u.id)
|
||||
.flatMap((u) => {
|
||||
const o = origLines.find((x) => x.id === u.id);
|
||||
if (!o) return [];
|
||||
const diffs = keysToTrack
|
||||
.filter((k) => String(o[k]) !== String(u[k]))
|
||||
.map((k) => `${fmtVal(k, o[k])} → ${fmtVal(k, u[k])}`);
|
||||
return diffs.length ? [`${u.line_desc || u.description || u.id}: ${diffs.join("; ")}`] : [];
|
||||
})
|
||||
];
|
||||
|
||||
[
|
||||
["added", addedObjs.filter((a) => !a.handled).map((a) => `+${a.label} (${a.vals})`)],
|
||||
["removed", removedObjs.filter((r) => !r.handled).map((r) => `-${r.label} (${r.vals})`)],
|
||||
["modified", modified]
|
||||
].forEach(([type, items]) => {
|
||||
if (items.length) changed.push(`billlines ${type}: ${items.join(" | ")}`);
|
||||
});
|
||||
|
||||
return changed.length ? changed.join("; ") : bill.invoice_number || "No changes";
|
||||
})();
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number, details)
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
type: "billupdated"
|
||||
});
|
||||
|
||||
await refetch();
|
||||
|
||||
@@ -14,19 +14,16 @@ import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobLineEditModal: selectJobLineEditModal,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
|
||||
});
|
||||
|
||||
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop }) {
|
||||
const {
|
||||
treatments: { CriticalPartsScanning }
|
||||
} = useTreatmentsWithConfig({
|
||||
@@ -77,16 +74,6 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
||||
notification.success({
|
||||
title: t("joblines.successes.created")
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobLineEditModal.context.jobid,
|
||||
operation: AuditTrailMapping.jobmanuallineinsert(
|
||||
Object.entries(values)
|
||||
.filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p")
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join("; ")
|
||||
),
|
||||
type: "jobmanuallineinsert"
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
title: t("joblines.errors.creating", {
|
||||
@@ -116,29 +103,6 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
||||
notification.success({
|
||||
title: t("joblines.successes.updated")
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobLineEditModal.context.jobid,
|
||||
operation: AuditTrailMapping.joblineupdate(
|
||||
(() => {
|
||||
const original = jobLineEditModal.context || {};
|
||||
const changed = Object.entries(values)
|
||||
.filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p")
|
||||
.map(([k, v]) => {
|
||||
const orig = original[k];
|
||||
if (String(orig) === String(v)) return null;
|
||||
const fmt = (key, val) => {
|
||||
if (val == null || val === "") return "<<empty>>";
|
||||
if (typeof val === "number" && key.toLowerCase().includes("price")) return `$${val.toFixed(2)}`;
|
||||
return val;
|
||||
};
|
||||
return `${k}: ${fmt(k, orig)} → ${fmt(k, v)}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return changed.length ? changed.join("; ") : "No changes";
|
||||
})()
|
||||
),
|
||||
type: "joblineupdate"
|
||||
});
|
||||
} else {
|
||||
notification.success({
|
||||
title: t("joblines.errors.updating", {
|
||||
|
||||
@@ -58,6 +58,7 @@ export function ProductionColumnsComponent({
|
||||
|
||||
const columnKeys = columns.map((i) => i.key);
|
||||
const cols = dataSource({
|
||||
bodyshop,
|
||||
technician,
|
||||
data,
|
||||
state: tableState,
|
||||
|
||||
@@ -609,7 +609,19 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
||||
ellipsis: true,
|
||||
|
||||
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
|
||||
}
|
||||
},
|
||||
...(bodyshop && bodyshop.rr_dealerid
|
||||
? [
|
||||
{
|
||||
title: i18n.t("jobs.fields.dms.id"),
|
||||
dataIndex: "dms_id",
|
||||
key: "dms_id",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.dms_id, b.dms_id),
|
||||
sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order
|
||||
}
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
export default productionListColumnsData;
|
||||
|
||||
@@ -244,6 +244,7 @@ export function ProductionListConfigManager({
|
||||
nextConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: ensureDefaultState(state),
|
||||
refetch,
|
||||
@@ -270,6 +271,7 @@ export function ProductionListConfigManager({
|
||||
activeConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: ensureDefaultState(state),
|
||||
refetch,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -15,26 +15,21 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
timeTicketModal: selectTimeTicket,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket"))
|
||||
});
|
||||
|
||||
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop }) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
|
||||
const lastSubmittedRef = useRef(null);
|
||||
|
||||
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
|
||||
|
||||
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
||||
@@ -55,8 +50,6 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
});
|
||||
|
||||
const handleFinish = (values) => {
|
||||
// Save submitted values so we can compute audit-trail details after the mutation completes
|
||||
lastSubmittedRef.current = values;
|
||||
setLoading(true);
|
||||
const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid);
|
||||
if (timeTicketModal.context.id) {
|
||||
@@ -96,44 +89,6 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
title: t("timetickets.successes.created")
|
||||
});
|
||||
|
||||
const timeticket = timeTicketModal.context?.timeticket ?? {};
|
||||
const original = timeticket || {};
|
||||
const submitted = lastSubmittedRef.current || {};
|
||||
|
||||
const fmt = (key, val) => {
|
||||
if (val == null || val === "") return "<<empty>>";
|
||||
const k = key.toLowerCase();
|
||||
if (dayjs.isDayjs?.(val)) return dayjs(val).format(k.includes("clock") ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD");
|
||||
if (typeof val === "number")
|
||||
return k.includes("hrs")
|
||||
? val.toFixed(1)
|
||||
: k.includes("rate") || k.includes("price")
|
||||
? `$${val.toFixed(2)}`
|
||||
: String(val);
|
||||
if (key === "employeeid") {
|
||||
const emp = EmployeeAutoCompleteData?.employees?.find(({ id }) => id === val);
|
||||
return emp ? `${emp.first_name} ${emp.last_name}` : String(val);
|
||||
}
|
||||
return String(val);
|
||||
};
|
||||
|
||||
const changed = Object.entries(submitted)
|
||||
.filter(([, v]) => v != null && v !== "")
|
||||
.map(([k, v]) => {
|
||||
const origVal = k === "jobid" ? (original.job?.id ?? original.jobid ?? original[k]) : original[k];
|
||||
return String(fmt(k, origVal)) !== String(fmt(k, v)) ? `${k}: ${fmt(k, origVal)} → ${fmt(k, v)}` : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: timeticket.job?.id ?? timeticket.jobid,
|
||||
operation: AuditTrailMapping.timeticketupdated(
|
||||
[original.employee.first_name, original.employee.last_name].filter(Boolean).join(" "),
|
||||
original.date ? dayjs(original.date).format("YYYY-MM-DD") : "<<empty>>",
|
||||
changed.length ? changed.join("; ") : "No changes"
|
||||
)
|
||||
});
|
||||
|
||||
// Refresh parent screens (Job Labor tab, etc.)
|
||||
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
|
||||
employee_prep
|
||||
employee_csr
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
@@ -269,6 +270,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
|
||||
employee_prep
|
||||
employee_csr
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
@@ -2671,6 +2673,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
suspended
|
||||
job_totals
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
|
||||
@@ -8,14 +8,13 @@ import { createStructuredSelector } from "reselect";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
||||
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsCreateComponent from "./jobs-create.component";
|
||||
import JobCreateContext from "./jobs-create.context";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -23,11 +22,10 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||
});
|
||||
|
||||
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser, insertAuditTrail }) {
|
||||
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) {
|
||||
const { t } = useTranslation();
|
||||
const notification = useNotification();
|
||||
|
||||
@@ -86,11 +84,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
||||
newJobId: resp.data.insert_jobs.returning[0].id
|
||||
});
|
||||
logImEXEvent("manual_job_create_completed", {});
|
||||
insertAuditTrail({
|
||||
jobid: resp.data.insert_jobs.returning[0].id,
|
||||
operation: AuditTrailMapping.jobmanualcreate(),
|
||||
type: "jobmanualcreate"
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
||||
"billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.",
|
||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||
"billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{values}}.",
|
||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
||||
"failedpayment": "Failed payment attempt.",
|
||||
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||
@@ -137,9 +137,6 @@
|
||||
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
|
||||
"jobinvoiced": "Job has been invoiced.",
|
||||
"jobioucreated": "IOU Created.",
|
||||
"joblineupdate": "Job line {{line_desc}} updated.",
|
||||
"jobmanualcreate": "Job manually created.",
|
||||
"jobmanuallineinsert": "Job line manually added with the following details: {{values}}.",
|
||||
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
||||
"jobnoteadded": "Note added to Job.",
|
||||
"jobnotedeleted": "Note deleted from Job.",
|
||||
@@ -155,8 +152,7 @@
|
||||
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
||||
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
||||
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}",
|
||||
"timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}"
|
||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
|
||||
@@ -137,9 +137,6 @@
|
||||
"jobintake": "",
|
||||
"jobinvoiced": "",
|
||||
"jobioucreated": "",
|
||||
"joblineupdate": "",
|
||||
"jobmanualcreate": "",
|
||||
"jobmanuallineinsert": "",
|
||||
"jobmodifylbradj": "",
|
||||
"jobnoteadded": "",
|
||||
"jobnotedeleted": "",
|
||||
@@ -155,8 +152,7 @@
|
||||
"tasks_deleted": "",
|
||||
"tasks_uncompleted": "",
|
||||
"tasks_undeleted": "",
|
||||
"tasks_updated": "",
|
||||
"timeticketupdated": ""
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
|
||||
@@ -137,9 +137,6 @@
|
||||
"jobintake": "",
|
||||
"jobinvoiced": "",
|
||||
"jobioucreated": "",
|
||||
"joblineupdate": "",
|
||||
"jobmanualcreate": "",
|
||||
"jobmanuallineinsert": "",
|
||||
"jobmodifylbradj": "",
|
||||
"jobnoteadded": "",
|
||||
"jobnotedeleted": "",
|
||||
@@ -155,8 +152,7 @@
|
||||
"tasks_deleted": "",
|
||||
"tasks_uncompleted": "",
|
||||
"tasks_undeleted": "",
|
||||
"tasks_updated": "",
|
||||
"timeticketupdated": ""
|
||||
"tasks_updated": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
|
||||
@@ -10,7 +10,7 @@ const AuditTrailMapping = {
|
||||
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||
billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }),
|
||||
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||
billupdated: (invoice_number, values) => i18n.t("audit_trail.messages.billupdated", { invoice_number, values }),
|
||||
billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }),
|
||||
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
||||
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
|
||||
jobchecklist: (type, inproduction, status) =>
|
||||
@@ -26,9 +26,6 @@ const AuditTrailMapping = {
|
||||
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
||||
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
||||
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
||||
joblineupdate: (line_desc) => i18n.t("audit_trail.messages.joblineupdate", { line_desc }),
|
||||
jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"),
|
||||
jobmanuallineinsert: (values) => i18n.t("audit_trail.messages.jobmanuallineinsert", { values }),
|
||||
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
||||
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
||||
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
||||
@@ -75,8 +72,7 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
||||
title,
|
||||
uncompletedBy
|
||||
}),
|
||||
timeticketupdated: (employee, date, details) => i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details })
|
||||
})
|
||||
};
|
||||
|
||||
export default AuditTrailMapping;
|
||||
|
||||
@@ -32,6 +32,7 @@ async function OpenSearchUpdateHandler(req, res) {
|
||||
clm_no
|
||||
clm_total
|
||||
comment
|
||||
dms_id
|
||||
ins_co_nm
|
||||
owner_owing
|
||||
ownr_co_nm
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
|
||||
const CHATTER_BASE_URL = process.env.CHATTER_API_BASE_URL || "https://api.chatterresearch.com";
|
||||
const AWS_REGION = process.env.AWS_REGION || "ca-central-1";
|
||||
|
||||
// Configure SecretsManager client with localstack support
|
||||
const secretsClientOptions = {
|
||||
region: AWS_REGION,
|
||||
region: InstanceRegion(),
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
secretsClientOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const secretsClient = new SecretsManagerClient(secretsClientOptions);
|
||||
|
||||
@@ -3,7 +3,7 @@ const Dinero = require("dinero.js");
|
||||
const moment = require("moment-timezone");
|
||||
const logger = require("../utils/logger");
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceIsLocalStackEnabled } = require("../utils/instanceMgr");
|
||||
const fs = require("fs");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail");
|
||||
@@ -35,10 +35,9 @@ const S3_BUCKET_NAME = InstanceManager({
|
||||
rome: "rome-carfax-uploads"
|
||||
});
|
||||
const region = InstanceManager.InstanceRegion;
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => {
|
||||
const webPath = isLocal
|
||||
const webPath = InstanceIsLocalStackEnabled()
|
||||
? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
|
||||
: `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const logger = require("../utils/logger");
|
||||
const fs = require("fs");
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
|
||||
let Client = require("ssh2-sftp-client");
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
@@ -151,10 +152,8 @@ async function getPrivateKey() {
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
secretsClientOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const client = new SecretsManagerClient(secretsClientOptions);
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { InstanceRegion } = require("../utils/instanceMgr");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
const aws = require("@aws-sdk/client-ses");
|
||||
const nodemailer = require("nodemailer");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
const sesConfig = {
|
||||
apiVersion: "latest",
|
||||
credentials: defaultProvider(),
|
||||
region: InstanceRegion()
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
sesConfig.endpoint = InstanceLocalStackEndpoint();
|
||||
logger.logger.debug(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2442,6 +2442,9 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
|
||||
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) {
|
||||
id
|
||||
shopid
|
||||
bodyshop {
|
||||
rr_dealerid
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { JOB_DOCUMENT_FIELDS, getGlobalSearchQueryStringFields } = require("./os-search-config");
|
||||
|
||||
async function OpenSearchUpdateHandler(req, res) {
|
||||
try {
|
||||
@@ -21,27 +22,7 @@ async function OpenSearchUpdateHandler(req, res) {
|
||||
|
||||
switch (req.body.table.name) {
|
||||
case "jobs":
|
||||
document = pick(req.body.event.data.new, [
|
||||
"id",
|
||||
"bodyshopid",
|
||||
"clm_no",
|
||||
"clm_total",
|
||||
"comment",
|
||||
"ins_co_nm",
|
||||
"owner_owing",
|
||||
"ownr_co_nm",
|
||||
"ownr_fn",
|
||||
"ownr_ln",
|
||||
"ownr_ph1",
|
||||
"ownr_ph2",
|
||||
"plate_no",
|
||||
"ro_number",
|
||||
"status",
|
||||
"v_model_yr",
|
||||
"v_make_desc",
|
||||
"v_model_desc",
|
||||
"v_vin"
|
||||
]);
|
||||
document = pick(req.body.event.data.new, JOB_DOCUMENT_FIELDS);
|
||||
document.bodyshopid = req.body.event.data.new.shopid;
|
||||
break;
|
||||
case "vehicles":
|
||||
@@ -197,15 +178,18 @@ async function OpenSearchSearchHandler(req, res) {
|
||||
user: req.user.email
|
||||
});
|
||||
|
||||
if (assocs.length === 0) {
|
||||
if (assocs.associations.length === 0) {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const osClient = await getClient();
|
||||
|
||||
const activeAssociation = assocs.associations[0];
|
||||
const bodyShopIdMatchOverride = isNil(process.env.BODY_SHOP_ID_MATCH_OVERRIDE)
|
||||
? assocs.associations[0].shopid
|
||||
? activeAssociation.shopid
|
||||
: process.env.BODY_SHOP_ID_MATCH_OVERRIDE;
|
||||
const isReynoldsEnabled = Boolean(activeAssociation.bodyshop?.rr_dealerid);
|
||||
|
||||
const { body } = await osClient.search({
|
||||
...(index ? { index } : { index: ["jobs", "vehicles", "owners", "bills", "payments"] }),
|
||||
@@ -241,21 +225,8 @@ async function OpenSearchSearchHandler(req, res) {
|
||||
query: `*${search}*`,
|
||||
// Weighted Fields
|
||||
fields: [
|
||||
"*ro_number^20",
|
||||
"*clm_no^14",
|
||||
"*v_vin^12",
|
||||
"*plate_no^12",
|
||||
"*ownr_ln^10",
|
||||
"transactionid^10",
|
||||
"paymentnum^10",
|
||||
"invoice_number^10",
|
||||
"*ownr_fn^8",
|
||||
"*ownr_co_nm^8",
|
||||
"*ownr_ph1^8",
|
||||
"*ownr_ph2^8",
|
||||
"*vendor.name^8",
|
||||
"*comment^6"
|
||||
// "*"
|
||||
...getGlobalSearchQueryStringFields({ isReynoldsEnabled })
|
||||
// "*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
69
server/opensearch/os-search-config.js
Normal file
69
server/opensearch/os-search-config.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Fields to be included in the job document indexed in OpenSearch. These fields are used for both indexing and
|
||||
* searching.
|
||||
* @type {string[]}
|
||||
*/
|
||||
const JOB_DOCUMENT_FIELDS = [
|
||||
"id",
|
||||
"bodyshopid",
|
||||
"clm_no",
|
||||
"clm_total",
|
||||
"comment",
|
||||
"dms_id",
|
||||
"ins_co_nm",
|
||||
"owner_owing",
|
||||
"ownr_co_nm",
|
||||
"ownr_fn",
|
||||
"ownr_ln",
|
||||
"ownr_ph1",
|
||||
"ownr_ph2",
|
||||
"plate_no",
|
||||
"ro_number",
|
||||
"status",
|
||||
"v_model_yr",
|
||||
"v_make_desc",
|
||||
"v_model_desc",
|
||||
"v_vin"
|
||||
];
|
||||
|
||||
/**
|
||||
* Fields to be included in the global search query string. These fields are used for constructing the search query.
|
||||
* @type {string[]}
|
||||
*/
|
||||
const BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS = [
|
||||
"*ro_number^20",
|
||||
"*clm_no^14",
|
||||
"*v_vin^12",
|
||||
"*plate_no^12",
|
||||
"*ownr_ln^10",
|
||||
"transactionid^10",
|
||||
"paymentnum^10",
|
||||
"invoice_number^10",
|
||||
"*ownr_fn^8",
|
||||
"*ownr_co_nm^8",
|
||||
"*ownr_ph1^8",
|
||||
"*ownr_ph2^8",
|
||||
"*vendor.name^8",
|
||||
"*comment^6"
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the fields to be included in the global search query string. If Reynolds is enabled, it includes the dms_id
|
||||
* field with a higher boost.
|
||||
* @param param0
|
||||
* @param param0.isReynoldsEnabled
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const getGlobalSearchQueryStringFields = ({ isReynoldsEnabled = false } = {}) => {
|
||||
if (!isReynoldsEnabled) {
|
||||
return BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS;
|
||||
}
|
||||
|
||||
return ["*dms_id^20", ...BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
JOB_DOCUMENT_FIELDS,
|
||||
BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS,
|
||||
getGlobalSearchQueryStringFields
|
||||
};
|
||||
21
server/opensearch/tests/os-search-config.test.js
Normal file
21
server/opensearch/tests/os-search-config.test.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { JOB_DOCUMENT_FIELDS, BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS, getGlobalSearchQueryStringFields } = require(
|
||||
"../os-search-config"
|
||||
);
|
||||
|
||||
describe("os-search-config", () => {
|
||||
it("indexes dms_id on job documents", () => {
|
||||
expect(JOB_DOCUMENT_FIELDS).toContain("dms_id");
|
||||
});
|
||||
|
||||
it("includes dms_id in global search fields for Reynolds shops", () => {
|
||||
expect(getGlobalSearchQueryStringFields({ isReynoldsEnabled: true })).toContain("*dms_id^20");
|
||||
});
|
||||
|
||||
it("keeps the default search fields unchanged for non-Reynolds shops", () => {
|
||||
expect(getGlobalSearchQueryStringFields()).toEqual(BASE_GLOBAL_SEARCH_QUERY_STRING_FIELDS);
|
||||
});
|
||||
});
|
||||
@@ -7,14 +7,24 @@
|
||||
* @property { string | object | function } promanager Return this prop if Rome.
|
||||
* @property { string | object | function } imex Return this prop if Rome.
|
||||
*/
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
|
||||
function InstanceManager({ args, instance, debug, executeFunction, rome, promanager, imex }) {
|
||||
/**
|
||||
* InstanceManager is a utility function that determines which property to return based on the current instance type.
|
||||
* @param param0
|
||||
* @param param0.args
|
||||
* @param param0.instance
|
||||
* @param param0.debug
|
||||
* @param param0.executeFunction
|
||||
* @param param0.rome
|
||||
* @param param0.promanager
|
||||
* @param param0.imex
|
||||
* @returns {*|null}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceManager = ({ args, instance, debug, executeFunction, rome, promanager, imex }) => {
|
||||
let propToReturn = null;
|
||||
|
||||
//TODO: Remove after debugging.
|
||||
if (promanager) {
|
||||
console.trace("ProManager Prop was used");
|
||||
}
|
||||
switch (instance || process.env.INSTANCE) {
|
||||
case "IMEX":
|
||||
propToReturn = imex;
|
||||
@@ -50,15 +60,42 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana
|
||||
}
|
||||
if (executeFunction && typeof propToReturn === "function") return propToReturn(...args);
|
||||
return propToReturn === undefined ? null : propToReturn;
|
||||
}
|
||||
};
|
||||
|
||||
exports.InstanceRegion = () =>
|
||||
/**
|
||||
* Returns the AWS region to be used for the current instance, which is determined by the INSTANCE environment variable.
|
||||
* @returns {*}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceRegion = () =>
|
||||
InstanceManager({
|
||||
imex: "ca-central-1",
|
||||
rome: "us-east-2"
|
||||
});
|
||||
|
||||
exports.InstanceEndpoints = () =>
|
||||
/**
|
||||
* Checks if the instance is configured to use LocalStack by verifying the presence of the LOCALSTACK_HOSTNAME
|
||||
* environment variable.
|
||||
* @returns {boolean}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceIsLocalStackEnabled = () =>
|
||||
isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
/**
|
||||
* Returns the LocalStack endpoint URL based on the LOCALSTACK_HOSTNAME environment variable.
|
||||
* @returns {`http://${*}:4566`}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceLocalStackEndpoint = () => `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
|
||||
/**
|
||||
* Returns the appropriate endpoints for the current instance, which can be used for making API calls or other network
|
||||
* requests.
|
||||
* @returns {*|null}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceEndpoints = () =>
|
||||
InstanceManager({
|
||||
imex:
|
||||
process.env?.NODE_ENV === "development"
|
||||
@@ -74,4 +111,11 @@ exports.InstanceEndpoints = () =>
|
||||
: "https://romeonline.io"
|
||||
});
|
||||
|
||||
exports.default = InstanceManager;
|
||||
module.exports = {
|
||||
InstanceManager,
|
||||
InstanceRegion,
|
||||
InstanceIsLocalStackEnabled,
|
||||
InstanceLocalStackEndpoint,
|
||||
InstanceEndpoints,
|
||||
default: InstanceManager
|
||||
};
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const winston = require("winston");
|
||||
const WinstonCloudWatch = require("winston-cloudwatch");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { uploadFileToS3 } = require("./s3");
|
||||
const { v4 } = require("uuid");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr");
|
||||
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
@@ -48,7 +47,7 @@ const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debu
|
||||
|
||||
const createLogger = () => {
|
||||
try {
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
const isLocal = InstanceIsLocalStackEnabled();
|
||||
const logGroupName = isLocal ? "development" : process.env.CLOUDWATCH_LOG_GROUP;
|
||||
|
||||
const winstonCloudwatchTransportDefaults = {
|
||||
@@ -60,7 +59,7 @@ const createLogger = () => {
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
winstonCloudwatchTransportDefaults.awsOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
winstonCloudwatchTransportDefaults.awsOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const levelFilter = (levels) => {
|
||||
|
||||
@@ -7,8 +7,7 @@ const {
|
||||
CopyObjectCommand
|
||||
} = require("@aws-sdk/client-s3");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr");
|
||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||
|
||||
const createS3Client = () => {
|
||||
@@ -17,10 +16,8 @@ const createS3Client = () => {
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
S3Options.endpoint = InstanceLocalStackEndpoint();
|
||||
S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname
|
||||
}
|
||||
|
||||
@@ -105,7 +102,7 @@ const createS3Client = () => {
|
||||
});
|
||||
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 360 });
|
||||
return presignedUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
uploadFileToS3,
|
||||
@@ -119,7 +116,4 @@ const createS3Client = () => {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = createS3Client();
|
||||
|
||||
Reference in New Issue
Block a user