Files
bodyshop/server/rr/rr-job-helpers.js

175 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/rr/rr-job-helpers.js
// Utilities to fetch and map job data into RR payloads using the shared Hasura client.
const client = require("../graphql-client/graphql-client").client;
const { GET_JOB_BY_PK } = require("../graphql-client/queries");
// ---------- Internals ----------
function digitsOnly(s) {
return String(s || "").replace(/[^\d]/g, "");
}
function pickJobId(ctx, explicitId) {
return explicitId || ctx?.job?.id || ctx?.payload?.job?.id || ctx?.payload?.jobId || ctx?.jobId || null;
}
function safeVin(job) {
// Your schema exposes v_vin on jobs (no vehicle_vin root field).
return (job?.v_vin && String(job.v_vin).trim()) || null;
}
// Combined search helpers expect array-like blocks
function blocksFromCombinedSearchResult(res) {
const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
return Array.isArray(data) ? data : [];
}
// ---------- Public API ----------
/**
* Fetch a job by id using the shared Hasura GraphQL client.
* Resolution order:
* 1) ctx.job
* 2) ctx.payload.job
* 3) ctx.payload.jobId / ctx.jobId / explicit jobId
*/
async function QueryJobData(ctx = {}, jobId) {
if (ctx?.job) return ctx.job;
if (ctx?.payload?.job) return ctx.payload.job;
const id = pickJobId(ctx, jobId);
if (!id) throw new Error("QueryJobData: jobId required (none found in ctx or args)");
try {
const res = await client.request(GET_JOB_BY_PK, { id });
const job = res?.jobs_by_pk;
if (!job) throw new Error(`Job ${id} not found`);
return job;
} catch (e) {
const msg = e?.response?.errors?.[0]?.message || e.message || "unknown";
throw new Error(`QueryJobData failed: ${msg}`);
}
}
/**
* Build minimal RR RO payload (keys match your RR clients expectations).
* Uses fields that exist in your schema (v_vin, ro_number, owner fields, etc).
*/
function buildRRRepairOrderPayload({ job, selectedCustomer, advisorNo }) {
const custNo =
(selectedCustomer && (selectedCustomer.custNo || selectedCustomer.customerNo)) ||
(typeof selectedCustomer === "string" || typeof selectedCustomer === "number" ? String(selectedCustomer) : null);
if (!custNo) throw new Error("No RR customer selected (custNo missing)");
const vin = safeVin(job);
// For RR create flows, VIN is typically required; leave null allowed if you gate earlier in your flow.
return {
repairOrderNumber: String(job?.ro_number || job?.job_number || job?.id),
deptType: "B",
vin: vin || undefined,
custNo,
advNo: advisorNo || undefined
};
}
/**
* Derive a vehicle search payload from a job.
* Prefers VIN; otherwise tries a plate, else null.
*/
function makeVehicleSearchPayloadFromJob(job) {
const vin = safeVin(job);
if (vin) return { kind: "vin", vin };
const plate = job?.plate_no;
if (plate) return { kind: "license", license: String(plate).trim() };
return null;
}
/**
* Derive a customer search payload from a job.
* Prefers phone (digits), then last name/company, then VIN.
*/
function makeCustomerSearchPayloadFromJob(job) {
const phone = job?.ownr_ph1;
const d = digitsOnly(phone);
if (d.length >= 7) return { kind: "phone", phone: d };
const lastName = job?.ownr_ln;
const company = job?.ownr_co_nm;
const lnOrCompany = lastName || company;
if (lnOrCompany) return { kind: "name", name: { name: String(lnOrCompany).trim() } };
const vin = safeVin(job);
if (vin) return { kind: "vin", vin };
return null;
}
/**
* Normalize candidate customers from a RR combined search response.
*/
function normalizeCustomerCandidates(res) {
const blocks = blocksFromCombinedSearchResult(res);
const out = [];
for (const blk of blocks) {
const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
const custNos = serv.map((sv) => sv?.VehicleServInfo?.CustomerNo).filter(Boolean);
const nci = blk?.NameContactId;
const ind = nci?.NameId?.IndName;
const bus = nci?.NameId?.BusName;
const personal = [ind?.FName, ind?.LName].filter(Boolean).join(" ").trim();
const company = bus?.CompanyName;
const name = (personal || company || "").trim();
for (const custNo of custNos) {
out.push({ custNo, name: name || `Customer ${custNo}`, _blk: blk });
}
}
const seen = new Set();
return out.filter((c) => {
if (!c.custNo || seen.has(c.custNo)) return false;
seen.add(c.custNo);
return true;
});
}
/**
* Normalize candidate vehicles from a RR combined search response.
*/
function normalizeVehicleCandidates(res) {
const blocks = blocksFromCombinedSearchResult(res);
const out = [];
for (const blk of blocks) {
const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
for (const sv of serv) {
const v = sv?.Vehicle || {};
const vin = v?.Vin || v?.VIN || v?.vin;
if (!vin) continue;
const year = v?.VehicleYr || v?.ModelYear || v?.Year;
const make = v?.VehicleMake || v?.MakeName || v?.Make;
const model = v?.MdlNo || v?.ModelDesc || v?.Model;
const label = [year, make, model, vin].filter(Boolean).join(" ");
out.push({ vin, year, make, model, label, _blk: blk });
}
}
const seen = new Set();
return out.filter((v) => {
if (!v.vin || seen.has(v.vin)) return false;
seen.add(v.vin);
return true;
});
}
module.exports = {
QueryJobData,
buildRRRepairOrderPayload,
makeCustomerSearchPayloadFromJob,
makeVehicleSearchPayloadFromJob,
normalizeCustomerCandidates,
normalizeVehicleCandidates
};