Merged in feature/IO-3255-simplified-part-management (pull request #2530)

Feature/IO-3255 simplified part management
This commit is contained in:
Dave Richer
2025-09-02 19:24:59 +00:00
7 changed files with 309 additions and 80 deletions

View File

@@ -12,13 +12,15 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component"; import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician technician: selectTechnician,
isPartsEntry: selectIsPartsEntry
}); });
const mapDispatchToProps = () => ({}); const mapDispatchToProps = () => ({});
export function JobLinesPartPriceChange({ job, line, refetch, technician }) { export function JobLinesPartPriceChange({ job, line, refetch, technician, isPartsEntry }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC); const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
const notification = useNotification(); const notification = useNotification();
@@ -64,6 +66,7 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
const popcontent = const popcontent =
!technician && !technician &&
!isPartsEntry &&
InstanceRenderManager({ InstanceRenderManager({
imex: null, imex: null,
rome: ( rome: (

View File

@@ -481,48 +481,50 @@ export function JobLinesComponent({
{Enhanced_Payroll.treatment === "on" && ( {Enhanced_Payroll.treatment === "on" && (
<JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} /> <JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} />
)} )}
<Button {!isPartsEntry && (
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician} <Button
onClick={() => { disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
setBillEnterContext({ onClick={() => {
actions: { refetch: refetch }, setBillEnterContext({
context: { actions: { refetch: refetch },
disableInvNumber: true, context: {
job: { id: job.id }, disableInvNumber: true,
bill: { job: { id: job.id },
vendorid: bodyshop.inhousevendorid, bill: {
invoice_number: "ih", vendorid: bodyshop.inhousevendorid,
isinhouse: true, invoice_number: "ih",
date: dayjs(), isinhouse: true,
total: 0, date: dayjs(),
billlines: selectedLines.map((p) => { total: 0,
return { billlines: selectedLines.map((p) => {
joblineid: p.id, return {
actual_price: p.act_price, joblineid: p.id,
actual_cost: 0, //p.act_price, actual_price: p.act_price,
line_desc: p.line_desc, actual_cost: 0, //p.act_price,
line_remarks: p.line_remarks, line_desc: p.line_desc,
part_type: p.part_type, line_remarks: p.line_remarks,
quantity: p.quantity || 1, part_type: p.part_type,
applicable_taxes: { quantity: p.quantity || 1,
local: false, applicable_taxes: {
state: false, local: false,
federal: false state: false,
} federal: false
}; }
}) };
})
}
} }
} });
});
//Clear out the selected lines. IO-785 //Clear out the selected lines. IO-785
setSelectedLines([]); setSelectedLines([]);
}} }}
> >
<HomeOutlined /> <HomeOutlined />
{t("parts.actions.orderinhouse")} {t("parts.actions.orderinhouse")}
{selectedLines.length > 0 && ` (${selectedLines.length})`} {selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button> </Button>
)}
<Button <Button
id="job-lines-order-parts-button" id="job-lines-order-parts-button"
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician} disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
@@ -578,7 +580,8 @@ export function JobLinesComponent({
{t("joblines.actions.new")} {t("joblines.actions.new")}
</Button> </Button>
)} )}
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })} {!isPartsEntry &&
InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
<JobCreateIOU job={job} selectedJobLines={selectedLines} /> <JobCreateIOU job={job} selectedJobLines={selectedLines} />
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}

View File

@@ -139,17 +139,18 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}> <DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
<ProductionListColumnComment record={job} /> <ProductionListColumnComment record={job} />
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel> {!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel> <DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull> <DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
{job.po_number} {job.po_number}
</DataLabel> </DataLabel>
<DataLabel label={t("jobs.fields.repairtotal")}> {!isPartsEntry && (
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter> <DataLabel label={t("jobs.fields.repairtotal")}>
<span style={{ margin: "0rem .5rem" }}>/</span> <CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter> <span style={{ margin: "0rem .5rem" }}>/</span>
</DataLabel> <CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
)}
{!isPartsEntry && ( {!isPartsEntry && (
<> <>
<DataLabel label={t("jobs.fields.alt_transport")}> <DataLabel label={t("jobs.fields.alt_transport")}>

View File

@@ -9,7 +9,6 @@ import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, statusSort } from "../../utils/sorters"; import { alphaSort, statusSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
@@ -144,26 +143,6 @@ export function SimplifiedPartsJobsListComponent({
sortOrder: sortcolumn === "clm_no" && sortorder, sortOrder: sortcolumn === "clm_no" && sortorder,
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}` 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
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
return record.clm_total ? (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : (
t("general.labels.unknown")
);
}
},
{ {
title: t("jobs.fields.partsstatus"), title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus", dataIndex: "partsstatus",

View File

@@ -0,0 +1,197 @@
{
"OP0": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP1": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP2": {
"desc": "REMOVE / INSTALL",
"opcode": "OP2",
"partcode": "PAE"
},
"OP3": {
"desc": "ADDITIONAL LABOR",
"opcode": "OP9",
"partcode": "PAE"
},
"OP4": {
"desc": "ALIGNMENT",
"opcode": "OP4",
"partcode": "PAS"
},
"OP5": {
"desc": "OVERHAUL",
"opcode": "OP5",
"partcode": "PAE"
},
"OP6": {
"desc": "REFINISH",
"opcode": "OP6",
"partcode": "PAE"
},
"OP7": {
"desc": "INSPECT",
"opcode": "OP7",
"partcode": "PAE"
},
"OP8": {
"desc": "CHECK / ADJUST",
"opcode": "OP8",
"partcode": "PAE"
},
"OP9": {
"desc": "REPAIR",
"opcode": "OP9",
"partcode": "PAE"
},
"OP10": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP11": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP12": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAN"
},
"OP13": {
"desc": "ADDITIONAL COSTS",
"opcode": "OP13",
"partcode": "PAE"
},
"OP14": {
"desc": "ADDITIONAL OPERATIONS",
"opcode": "OP14",
"partcode": "PAE"
},
"OP15": {
"desc": "BLEND",
"opcode": "OP15",
"partcode": "PAE"
},
"OP16": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAS"
},
"OP17": {
"desc": "POLICY LIMIT ADJUSTMENT",
"opcode": "OP9",
"partcode": "PAE"
},
"OP18": {
"desc": "APPEAR ALLOWANCE",
"opcode": "OP7",
"partcode": "PAE"
},
"OP20": {
"desc": "REMOVE AND REINSTALL",
"opcode": "OP20",
"partcode": "PAE"
},
"OP24": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP25": {
"desc": "TWO TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP26": {
"desc": "PAINTLESS DENT REPAIR",
"opcode": "OP16",
"partcode": "PAE"
},
"OP100": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP101": {
"desc": "REMOVE/REPLACE RECYCLED PART",
"opcode": "OP11",
"partcode": "PAL"
},
"OP103": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP104": {
"desc": "REMOVE / REPLACE PARTIAL LABOUR",
"opcode": "OP11",
"partcode": "PAA"
},
"OP105": {
"desc": "!!ADJUST MANUALLY!!",
"opcode": "OP99",
"partcode": "PAE"
},
"OP106": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP107": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP108": {
"desc": "MULTI TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP109": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP110": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP111": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP112": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAA"
},
"OP113": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP114": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP120": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP260": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAE"
}
}

View File

@@ -129,15 +129,17 @@ const partsManagementDeprovisioning = async (req, res) => {
const deletedUsers = []; const deletedUsers = [];
for (const user of associatedUsers) { for (const user of associatedUsers) {
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email }); const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
const assocCount = countResp.associations_aggregate.aggregate.count; // Determine which users now have zero associations and should be deleted (defer deletion until end)
if (assocCount === 0) { const emailsToAuthId = associatedUsers.reduce((acc, u) => {
await client.request(DELETE_USER, { email: user.email }); acc[u.email] = u.authId;
await deleteFirebaseUser(user.authId); return acc;
deletedUsers.push(user.email); }, {});
} const emailsToDelete = [];
await client.request(DELETE_USER, { email: user.email });
await deleteFirebaseUser(user.authId);
deletedUsers.push(user.email);
} }
emailsToDelete.push(user.email);
// Get all job ids for this shop, then delete joblines and jobs (joblines first)
const jobIds = await getJobIdsForShop(body.shopId); const jobIds = await getJobIdsForShop(body.shopId);
const joblinesDeleted = await deleteJoblinesForJobs(jobIds); const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
const jobsDeleted = await deleteJobsByIds(jobIds); const jobsDeleted = await deleteJobsByIds(jobIds);
@@ -174,6 +176,16 @@ const partsManagementDeprovisioning = async (req, res) => {
deletedJobsCount: jobsDeleted, deletedJobsCount: jobsDeleted,
deletedAuditTrailCount: auditDeleted deletedAuditTrailCount: auditDeleted
}); });
// Now delete users that have no remaining associations and their Firebase accounts
const deletedUsers = [];
for (const email of emailsToDelete) {
await client.request(DELETE_USER, { email });
const authId = emailsToAuthId[email];
if (authId) {
await deleteFirebaseUser(authId);
}
deletedUsers.push(email);
}
} catch (err) { } catch (err) {
logger.log("admin-delete-shop-error", "error", null, null, { logger.log("admin-delete-shop-error", "error", null, null, {
message: err.message, message: err.message,

View File

@@ -216,6 +216,36 @@ const GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ = `
} }
`; `;
// Clear task links to parts orders for all jobs in a shop to avoid FK violations when deleting parts orders
const CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS = `
mutation ClearTasksPartsOrderLinks($jobIds: [uuid!]!) {
update_tasks(
where: { parts_order: { jobid: { _in: $jobIds } } },
_set: { partsorderid: null }
) {
affected_rows
}
}
`;
// Delete parts order lines where the parent order belongs to any of the provided job IDs
const DELETE_PARTS_ORDER_LINES_BY_JOB_IDS = `
mutation DeletePartsOrderLinesByJobIds($jobIds: [uuid!]!) {
delete_parts_order_lines(where: { parts_order: { jobid: { _in: $jobIds } } }) {
affected_rows
}
}
`;
// Delete parts orders for the given job IDs
const DELETE_PARTS_ORDERS_BY_JOB_IDS = `
mutation DeletePartsOrdersByJobIds($jobIds: [uuid!]!) {
delete_parts_orders(where: { jobid: { _in: $jobIds } }) {
affected_rows
}
}
`;
module.exports = { module.exports = {
GET_BODYSHOP_STATUS, GET_BODYSHOP_STATUS,
GET_VEHICLE_BY_SHOP_VIN, GET_VEHICLE_BY_SHOP_VIN,
@@ -241,5 +271,9 @@ module.exports = {
DELETE_JOBS_BY_IDS, DELETE_JOBS_BY_IDS,
DELETE_AUDIT_TRAIL_BY_SHOP, DELETE_AUDIT_TRAIL_BY_SHOP,
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ, GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
GET_JOB_BY_ID GET_JOB_BY_ID,
// newly added exports
CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS,
DELETE_PARTS_ORDER_LINES_BY_JOB_IDS,
DELETE_PARTS_ORDERS_BY_JOB_IDS
}; };