import { Alert, Button, Card, Tabs, Typography } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component"; import { SyncOutlined } from "@ant-design/icons"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { resolveRROpCodeFromBodyshop } from "../../utils/dmsUtils.js"; import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(RrAllocationsSummary); /** * Normalize job allocations into a flat list for display / preview building. * @param ack * @returns {{ * center: *, * sale: *, * partsSale: *, * partsTaxableSale: *, * partsNonTaxableSale: *, * laborTaxableSale: *, * laborNonTaxableSale: *, * extrasSale: *, * extrasTaxableSale: *, * extrasNonTaxableSale: *, * cost: *, * profitCenter: *, * costCenter: * * }[]|*[]} */ function normalizeJobAllocations(ack) { if (!ack || !Array.isArray(ack.jobAllocations)) return []; return ack.jobAllocations.map((row) => ({ center: row.center, // legacy "sale" (total) if we ever want to show it again sale: row.sale || row.totalSale || null, // bucketed sales used to build split ROGOG/ROLABOR partsSale: row.partsSale || null, partsTaxableSale: row.partsTaxableSale || null, partsNonTaxableSale: row.partsNonTaxableSale || null, laborTaxableSale: row.laborTaxableSale || null, laborNonTaxableSale: row.laborNonTaxableSale || null, extrasSale: row.extrasSale || null, extrasTaxableSale: row.extrasTaxableSale || null, extrasNonTaxableSale: row.extrasNonTaxableSale || null, cost: row.cost || null, profitCenter: row.profitCenter || null, costCenter: row.costCenter || null })); } /** * RR-specific DMS Allocations Summary * Focused on what we actually send to RR: * - ROGOG (split by taxable / non-taxable segments) * - ROLABOR shell * * The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags) * is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog. * This component just renders the preview from `ack.rogg` / `ack.rolabor`. */ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocationsChange, opCode }) { const { t } = useTranslation(); const [roggPreview, setRoggPreview] = useState(null); const [rolaborPreview, setRolaborPreview] = useState(null); const [error, setError] = useState(null); const socketRef = useRef(socket); useEffect(() => { socketRef.current = socket; }, [socket]); // Prefer the user-selected OpCode (from DmsContainer), fall back to config default const effectiveOpCode = useMemo(() => opCode || resolveRROpCodeFromBodyshop(bodyshop), [opCode, bodyshop]); const fetchAllocations = useCallback(() => { if (!socket || !jobId) return; try { socket.emit("rr-calculate-allocations", { jobId, opCode: effectiveOpCode }, (ack) => { if (ack && ack.ok === false) { setRoggPreview(null); setRolaborPreview(null); setError(ack.error || t("dms.labels.allocations_error")); if (socketRef.current) { socketRef.current.allocationsSummary = []; socketRef.current.rrAllocationsRaw = ack; } if (onAllocationsChange) { onAllocationsChange([]); } return; } const jobAllocRows = normalizeJobAllocations(ack); setRoggPreview(ack?.rogg || null); setRolaborPreview(ack?.rolabor || null); setError(null); if (socketRef.current) { socketRef.current.allocationsSummary = jobAllocRows; socketRef.current.rrAllocationsRaw = ack; } if (onAllocationsChange) { onAllocationsChange(jobAllocRows); } }); } catch { setRoggPreview(null); setRolaborPreview(null); setError(t("dms.labels.allocations_error")); if (socketRef.current) { socketRef.current.allocationsSummary = []; } if (onAllocationsChange) { onAllocationsChange([]); } } }, [socket, jobId, t, onAllocationsChange, effectiveOpCode]); useEffect(() => { fetchAllocations(); }, [fetchAllocations]); const segmentLabelMap = { partsTaxable: "Parts Taxable", partsNonTaxable: "Parts Non-Taxable", extrasTaxable: "Extras Taxable", extrasNonTaxable: "Extras Non-Taxable", laborTaxable: "Labor Taxable", laborNonTaxable: "Labor Non-Taxable" }; const roggRows = useMemo(() => { if (!roggPreview || !Array.isArray(roggPreview.ops)) return []; const rows = []; roggPreview.ops.forEach((op) => { const rowOpCode = opCode || op.opCode; (op.lines || []).forEach((line, idx) => { const baseDesc = line.itemDesc; const segmentKind = op.segmentKind; const segmentCount = op.segmentCount || 0; const segmentLabel = segmentLabelMap[segmentKind] || segmentKind; const displayDesc = segmentCount > 1 && segmentLabel ? `${baseDesc} (${segmentLabel})` : baseDesc; rows.push({ key: `${op.jobNo}-${idx}`, opCode: rowOpCode, jobNo: op.jobNo, breakOut: line.breakOut, itemType: line.itemType, itemDesc: displayDesc, custQty: line.custQty, custPayTypeFlag: line.custPayTypeFlag, custTxblNtxblFlag: line.custTxblNtxblFlag, custPrice: line.amount?.custPrice, dlrCost: line.amount?.dlrCost, // segment metadata for visual styling segmentKind, segmentCount }); }); }); return rows; }, [roggPreview, opCode, segmentLabelMap]); const rolaborRows = useMemo(() => { if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return []; return rolaborPreview.ops.map((op, idx) => { const rowOpCode = opCode || op.opCode; return { key: `${op.jobNo}-${idx}`, opCode: rowOpCode, jobNo: op.jobNo, custPayTypeFlag: op.custPayTypeFlag, custTxblNtxblFlag: op.custTxblNtxblFlag, payType: op.bill?.payType, amtType: op.amount?.amtType, custPrice: op.amount?.custPrice, totalAmt: op.amount?.totalAmt }; }); }, [rolaborPreview, opCode]); // Totals for ROGOG (sum custPrice + dlrCost over all lines) const roggTotals = useMemo(() => { if (!roggPreview || !Array.isArray(roggPreview.ops)) { return { totalCustPrice: "0.00", totalDlrCost: "0.00" }; } let totalCustCents = 0; let totalCostCents = 0; roggPreview.ops.forEach((op) => { (op.lines || []).forEach((line) => { const cp = parseFloat(line.amount?.custPrice || "0"); if (!Number.isNaN(cp)) { totalCustCents += Math.round(cp * 100); } const dc = parseFloat(line.amount?.dlrCost || "0"); if (!Number.isNaN(dc)) { totalCostCents += Math.round(dc * 100); } }); }); return { totalCustPrice: (totalCustCents / 100).toFixed(2), totalDlrCost: (totalCostCents / 100).toFixed(2) }; }, [roggPreview]); const roggColumns = [ { title: "JobNo", dataIndex: "jobNo", key: "jobNo" }, { title: "OpCode", dataIndex: "opCode", key: "opCode" }, { title: "BreakOut", dataIndex: "breakOut", key: "breakOut" }, { title: "ItemType", dataIndex: "itemType", key: "itemType" }, { title: "ItemDesc", dataIndex: "itemDesc", key: "itemDesc" }, { title: "CustQty", dataIndex: "custQty", key: "custQty" }, { title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" }, { title: "CustPrice", dataIndex: "custPrice", key: "custPrice" }, { title: "DlrCost", dataIndex: "dlrCost", key: "dlrCost" } ]; const rolaborColumns = [ { title: "JobNo", dataIndex: "jobNo", key: "jobNo" }, { title: "OpCode", dataIndex: "opCode", key: "opCode" }, { title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" }, { title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" }, { title: "PayType", dataIndex: "payType", key: "payType" }, { title: "AmtType", dataIndex: "amtType", key: "amtType" }, { title: "CustPrice", dataIndex: "custPrice", key: "custPrice" }, { title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" } ]; const tabItems = [ { key: "rogog", label: "ROGOG Preview", children: ( <> OpCode: {effectiveOpCode}. Only centers with RR GOG mapping (rr_gogcode & rr_item_type) are included. Totals below reflect exactly what will be sent in ROGOG, with parts, extras, and labor split into taxable / non-taxable segments. { if ( record.segmentCount > 1 && (record.segmentKind === "laborTaxable" || record.segmentKind === "laborNonTaxable") ) { return "rr-allocations-tax-split-row"; } if (record.segmentCount > 1) { return "rr-allocations-split-row"; } return ""; }} summary={() => { const hasCustTotal = Number(roggTotals.totalCustPrice) !== 0; const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0; return ( {t("general.labels.totals")} {hasCustTotal ? roggTotals.totalCustPrice : null} {hasCostTotal ? roggTotals.totalDlrCost : null} ); }} /> ) }, { key: "rolabor", label: "ROLABOR Preview", children: ( <> This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG. ) } ]; return ( } />} > {bodyshop.pbs_configuration?.disablebillwip && ( )} {error && } ); }