Merged in feature/IO-3401-Parts-Rec-Enhanced (pull request #2743)
feature/IO-3401-Parts-Rec-Enhanced - Implement
This commit is contained in:
@@ -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);
|
||||||
@@ -16,11 +16,11 @@ import { pageLimit } from "../../utils/config";
|
|||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import AlertComponent from "../alert/alert.component";
|
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 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 OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import JobPartsReceived from "../job-parts-received/job-parts-received.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -235,7 +235,9 @@ export function PartsQueueListComponent({ bodyshop }) {
|
|||||||
title: t("jobs.fields.partsstatus"),
|
title: t("jobs.fields.partsstatus"),
|
||||||
dataIndex: "partsstatus",
|
dataIndex: "partsstatus",
|
||||||
key: "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"),
|
title: t("jobs.fields.comment"),
|
||||||
|
|||||||
@@ -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 OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||||
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
|
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
|
||||||
|
import ProductionListColumnPartsReceived from "../production-list-columns/production-list-columns.partsreceived.component";
|
||||||
|
|
||||||
const cardColor = (ssbuckets, totalHrs) => {
|
const cardColor = (ssbuckets, totalHrs) => {
|
||||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||||
@@ -312,6 +313,20 @@ const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
|||||||
</Col>
|
</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 }) {
|
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { metadata } = card;
|
const { metadata } = card;
|
||||||
@@ -411,6 +426,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
|
|||||||
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
||||||
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||||
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<PartsReceivedComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ const InformationSettings = ({ t }) => (
|
|||||||
"partsstatus",
|
"partsstatus",
|
||||||
"estimator",
|
"estimator",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
"tasks"
|
"tasks",
|
||||||
|
"partsreceived"
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
<Col xs={24} sm={12} md={8} lg={6} key={item}>
|
||||||
<Form.Item name={item} valuePropName="checked">
|
<Form.Item name={item} valuePropName="checked">
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const defaultKanbanSettings = {
|
|||||||
cardSize: "small",
|
cardSize: "small",
|
||||||
model_info: true,
|
model_info: true,
|
||||||
kiosk: false,
|
kiosk: false,
|
||||||
|
partsreceived: false,
|
||||||
totalHrs: true,
|
totalHrs: true,
|
||||||
totalAmountInProduction: false,
|
totalAmountInProduction: false,
|
||||||
totalLAB: true,
|
totalLAB: true,
|
||||||
|
|||||||
@@ -1,33 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import JobPartsReceived from "../job-parts-received/job-parts-received.component";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
export default function ProductionListColumnPartsReceived({ record }) {
|
||||||
bodyshop: selectBodyshop
|
return <JobPartsReceived parts={record.joblines_status} displayMode="full" popoverPlacement="topLeft" />;
|
||||||
});
|
|
||||||
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})`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2950,6 +2950,8 @@
|
|||||||
"settings": "Error saving board settings: {{error}}"
|
"settings": "Error saving board settings: {{error}}"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"click_for_statuses": "Click to view parts statuses",
|
||||||
|
"partsreceived": "Parts Received",
|
||||||
"actual_in": "Actual In",
|
"actual_in": "Actual In",
|
||||||
"addnewprofile": "Add New Profile",
|
"addnewprofile": "Add New Profile",
|
||||||
"alert": "Alert",
|
"alert": "Alert",
|
||||||
|
|||||||
@@ -2950,6 +2950,8 @@
|
|||||||
"settings": ""
|
"settings": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"click_for_statuses": "",
|
||||||
|
"partsreceived": "",
|
||||||
"actual_in": "",
|
"actual_in": "",
|
||||||
"addnewprofile": "",
|
"addnewprofile": "",
|
||||||
"alert": "",
|
"alert": "",
|
||||||
|
|||||||
@@ -2950,6 +2950,8 @@
|
|||||||
"settings": ""
|
"settings": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"click_for_statuses": "",
|
||||||
|
"partsreceived": "",
|
||||||
"actual_in": "",
|
"actual_in": "",
|
||||||
"addnewprofile": "",
|
"addnewprofile": "",
|
||||||
"alert": "",
|
"alert": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user