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": "",