116 lines
3.3 KiB
JavaScript
116 lines
3.3 KiB
JavaScript
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
|
|
* @param countsOnly
|
|
* @returns {JSX.Element}
|
|
* @constructor
|
|
*/
|
|
export function JobPartsReceived({
|
|
bodyshop,
|
|
parts,
|
|
displayMode = "full",
|
|
popoverPlacement = "top",
|
|
countsOnly = false
|
|
}) {
|
|
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]
|
|
);
|
|
|
|
if (countsOnly) return <JobPartsQueueCount parts={parts} />;
|
|
|
|
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}>
|
|
<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,
|
|
countsOnly: PropTypes.bool
|
|
};
|
|
|
|
export default connect(mapStateToProps)(JobPartsReceived);
|