Merged in feature/IO-3401-Parts-Rec-Enhanced (pull request #2743)

feature/IO-3401-Parts-Rec-Enhanced - Implement
This commit is contained in:
Dave Richer
2025-12-23 16:28:00 +00:00
9 changed files with 137 additions and 34 deletions

View File

@@ -0,0 +1,105 @@
import { useCallback, useMemo, useState } from "react";
import PropTypes from "prop-types";
import { Popover } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
/**
* Displays "Parts Received" summary (modeled after the Production Board List column),
* and on click shows a popover with the Parts Status grid (existing JobPartsQueueCount UI).
* @param bodyshop
* @param parts
* @param displayMode
* @param popoverPlacement
* @returns {JSX.Element}
* @constructor
*/
export function JobPartsReceived({ bodyshop, parts, displayMode = "full", popoverPlacement = "top" }) {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const summary = useMemo(() => {
const receivedStatus = bodyshop?.md_order_statuses?.default_received;
if (!Array.isArray(parts) || parts.length === 0 || !receivedStatus) {
return { total: 0, received: 0, percentLabel: t("general.labels.na") };
}
// Keep consistent with JobPartsQueueCount: exclude PAS / PASL from parts math
const { total, received } = parts.reduce(
(acc, val) => {
if (val?.part_type === "PAS" || val?.part_type === "PASL") return acc;
const count = Number(val?.count || 0);
acc.total += count;
if (val?.status === receivedStatus) {
acc.received += count;
}
return acc;
},
{ total: 0, received: 0 }
);
const percentLabel = total > 0 ? `${Math.round((received / total) * 100)}%` : t("general.labels.na");
return { total, received, percentLabel };
}, [parts, bodyshop?.md_order_statuses?.default_received]);
const canOpen = summary.total > 0;
const handleOpenChange = useCallback(
(nextOpen) => {
if (!canOpen) return;
setOpen(nextOpen);
},
[canOpen]
);
const displayText =
displayMode === "compact" ? summary.percentLabel : `${summary.percentLabel} (${summary.received}/${summary.total})`;
// Prevent row/cell click handlers (table selection, drawer selection, etc.)
const stop = (e) => e.stopPropagation();
return (
<Popover
open={open}
onOpenChange={handleOpenChange}
trigger={["click"]}
placement={popoverPlacement}
content={
<div onClick={stop} style={{ minWidth: 260 }}>
<JobPartsQueueCount parts={parts} />
</div>
}
>
<div
onClick={stop}
style={{
width: "100%",
height: "19px",
cursor: canOpen ? "pointer" : "default",
userSelect: "none"
}}
title={canOpen ? t("production.labels.click_for_statuses") : undefined}
>
{displayText}
</div>
</Popover>
);
}
JobPartsReceived.propTypes = {
bodyshop: PropTypes.object,
parts: PropTypes.array,
displayMode: PropTypes.oneOf(["full", "compact"]),
popoverPlacement: PropTypes.string
};
export default connect(mapStateToProps)(JobPartsReceived);

View File

@@ -16,11 +16,11 @@ import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import JobPartsReceived from "../job-parts-received/job-parts-received.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -235,7 +235,9 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
render: (text, record) => (
<JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" />
)
},
{
title: t("jobs.fields.comment"),

View File

@@ -19,6 +19,7 @@ import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.c
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
import ProductionListColumnPartsReceived from "../production-list-columns/production-list-columns.partsreceived.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -312,6 +313,20 @@ const TasksToolTip = ({ metadata, cardSettings, t }) =>
</Col>
);
const PartsReceivedComponent = ({ metadata, cardSettings, card }) =>
cardSettings?.partsreceived && (
<Col span={24} style={{ textAlign: "center" }}>
<ProductionListColumnPartsReceived
displayMode="full"
popoverPlacement="topLeft"
record={{
...metadata,
id: card?.id,
refetch: card?.refetch
}}
/>
</Col>
);
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
const { t } = useTranslation();
const { metadata } = card;
@@ -411,6 +426,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
<PartsReceivedComponent metadata={metadata} cardSettings={cardSettings} card={card} />
</Row>
);

View File

@@ -18,7 +18,8 @@ const InformationSettings = ({ t }) => (
"partsstatus",
"estimator",
"subtotal",
"tasks"
"tasks",
"partsreceived"
].map((item) => (
<Col xs={24} sm={12} md={8} lg={6} key={item}>
<Form.Item name={item} valuePropName="checked">

View File

@@ -74,6 +74,7 @@ const defaultKanbanSettings = {
cardSize: "small",
model_info: true,
kiosk: false,
partsreceived: false,
totalHrs: true,
totalAmountInProduction: false,
totalLAB: true,

View File

@@ -1,33 +1,5 @@
import { useMemo } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobPartsReceived from "../job-parts-received/job-parts-received.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListColumnPartsReceived);
export function ProductionListColumnPartsReceived({ bodyshop, record }) {
const amount = useMemo(() => {
const amount = record.joblines_status.reduce(
(acc, val) => {
acc.total += val.count;
acc.received =
val.status === bodyshop.md_order_statuses.default_received ? acc.received + val.count : acc.received;
return acc;
},
{ total: 0, received: 0 }
);
return {
...amount,
percent: amount.total !== 0 ? ((amount.received / amount.total) * 100).toFixed(0) + "%" : "N/A"
};
}, [record, bodyshop.md_order_statuses]);
return `${amount.percent} (${amount.received}/${amount.total})`;
export default function ProductionListColumnPartsReceived({ record }) {
return <JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" />;
}

View File

@@ -2950,6 +2950,8 @@
"settings": "Error saving board settings: {{error}}"
},
"labels": {
"click_for_statuses": "Click to view parts statuses",
"partsreceived": "Parts Received",
"actual_in": "Actual In",
"addnewprofile": "Add New Profile",
"alert": "Alert",

View File

@@ -2950,6 +2950,8 @@
"settings": ""
},
"labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",

View File

@@ -2950,6 +2950,8 @@
"settings": ""
},
"labels": {
"click_for_statuses": "",
"partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",