diff --git a/client/src/components/job-parts-received/job-parts-received.component.jsx b/client/src/components/job-parts-received/job-parts-received.component.jsx
new file mode 100644
index 000000000..64521fb5f
--- /dev/null
+++ b/client/src/components/job-parts-received/job-parts-received.component.jsx
@@ -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 (
+
+
+
+ }
+ >
+
+ {displayText}
+
+
+ );
+}
+
+JobPartsReceived.propTypes = {
+ bodyshop: PropTypes.object,
+ parts: PropTypes.array,
+ displayMode: PropTypes.oneOf(["full", "compact"]),
+ popoverPlacement: PropTypes.string
+};
+
+export default connect(mapStateToProps)(JobPartsReceived);
diff --git a/client/src/components/parts-queue-list/parts-queue.list.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
index a947eaefa..ff47d00fb 100644
--- a/client/src/components/parts-queue-list/parts-queue.list.component.jsx
+++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
@@ -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) =>
+ render: (text, record) => (
+
+ )
},
{
title: t("jobs.fields.comment"),
diff --git a/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx b/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
index 64cd69f9b..6f8d6d4a4 100644
--- a/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
+++ b/client/src/components/production-board-kanban/production-board-kanban-card.component.jsx
@@ -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 }) =>
);
+const PartsReceivedComponent = ({ metadata, cardSettings, card }) =>
+ cardSettings?.partsreceived && (
+
+
+
+ );
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
+
);
diff --git a/client/src/components/production-board-kanban/settings/InformationSettings.jsx b/client/src/components/production-board-kanban/settings/InformationSettings.jsx
index ddf88c987..4ae4e781a 100644
--- a/client/src/components/production-board-kanban/settings/InformationSettings.jsx
+++ b/client/src/components/production-board-kanban/settings/InformationSettings.jsx
@@ -18,7 +18,8 @@ const InformationSettings = ({ t }) => (
"partsstatus",
"estimator",
"subtotal",
- "tasks"
+ "tasks",
+ "partsreceived"
].map((item) => (
diff --git a/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js b/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
index 3c14a08f7..1c7a264b7 100644
--- a/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
+++ b/client/src/components/production-board-kanban/settings/defaultKanbanSettings.js
@@ -74,6 +74,7 @@ const defaultKanbanSettings = {
cardSize: "small",
model_info: true,
kiosk: false,
+ partsreceived: false,
totalHrs: true,
totalAmountInProduction: false,
totalLAB: true,
diff --git a/client/src/components/production-list-columns/production-list-columns.partsreceived.component.jsx b/client/src/components/production-list-columns/production-list-columns.partsreceived.component.jsx
index b1bf59f99..c4e3a0f6b 100644
--- a/client/src/components/production-list-columns/production-list-columns.partsreceived.component.jsx
+++ b/client/src/components/production-list-columns/production-list-columns.partsreceived.component.jsx
@@ -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 ;
}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index f79a1f166..7a8d5e8ce 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -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",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 2a9a6229b..aa2299846 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -2950,6 +2950,8 @@
"settings": ""
},
"labels": {
+ "click_for_statuses": "",
+ "partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 939f7752e..91d2ab205 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -2950,6 +2950,8 @@
"settings": ""
},
"labels": {
+ "click_for_statuses": "",
+ "partsreceived": "",
"actual_in": "",
"addnewprofile": "",
"alert": "",