IO-3255 Initial parts management changes.

This commit is contained in:
Patrick Fic
2025-06-11 10:29:58 -07:00
parent d835021069
commit 92369fceba
15 changed files with 1054 additions and 73 deletions

View File

@@ -0,0 +1,214 @@
import { BarsOutlined, PrinterFilled, SyncOutlined, ToolFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useQuery } from "@apollo/client";
import { Button, Divider, Form, Space, Tabs } from "antd";
import Axios from "axios";
import _ from "lodash";
import queryString from "query-string";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component.jsx";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container.jsx";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx";
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component.jsx";
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component.jsx";
import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx";
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component.jsx";
import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container.jsx";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
import { selectJobReadOnly } from "../../redux/application/application.selectors.js";
import { setModalContext } from "../../redux/modals/modals.actions.js";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
import dayjs from "../../utils/day.js";
import UndefinedToNull from "../../utils/undefinedtonull.js";
import { transformJobToForm } from "../jobs-detail/jobs-detail.page.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function SimplifiedPartsJobDetailComponent({
bodyshop,
setPrintCenterContext,
jobRO,
job,
mutationUpdateJob,
insertAuditTrail,
refetch
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const history = useNavigate();
const [loading, setLoading] = useState(false);
const search = queryString.parse(useLocation().search);
const formItemLayout = {
layout: "vertical"
};
const billsQuery = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const notification = useNotification();
const { scenarioNotificationsOn } = useSocket();
useEffect(() => {
//form.setFieldsValue(transormJobToForm(job));
form.resetFields();
}, [form, job]);
const handleBillOnRowClick = (record) => {
if (record) {
if (record.id) {
search.billid = record.id;
history({ search: queryString.stringify(search) });
}
} else {
delete search.billid;
history({ search: queryString.stringify(search) });
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history({ search: queryString.stringify(search) });
}
};
const handlePartsDispatchOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsdispatchid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsdispatchid;
history.push({ search: queryString.stringify(search) });
}
};
const menuExtra = (
<Space wrap>
<Button
onClick={() => {
refetch();
}}
key="refresh"
>
<SyncOutlined />
{t("general.labels.refresh")}
</Button>
<JobsChangeStatus job={job} />
<Button
onClick={() => {
setPrintCenterContext({
actions: { refetch: refetch },
context: {
id: job.id,
job: job,
type: "job"
}
});
}}
key="printing"
>
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />
<Button type="primary" loading={loading} disabled={jobRO} onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
</Space>
);
return (
<div>
<JobLineUpsertModalContainer />
<PageHeader title={<Space>{job.ro_number || t("general.labels.na")}</Space>} extra={menuExtra} />
<JobsDetailHeader job={job} />
<Divider type="horizontal" />
<JobProfileDataWarning job={job} />
<FormFieldsChanged form={form} />
<Tabs
defaultActiveKey={search.tab}
onChange={(key) => history({ search: `?tab=${key}` })}
tabBarStyle={{ fontWeight: "bold", borderBottom: "10px" }}
items={[
{
key: "repairdata",
icon: <BarsOutlined />,
id: "job-details-repairdata",
label: t("menus.jobsdetail.repairdata"),
forceRender: true,
children: (
<JobsLinesContainer
job={job}
joblines={job.joblines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
refetch={refetch}
form={form}
simple
/>
)
},
{
key: "partssublet",
id: "job-details-partssublet",
icon: <ToolFilled />,
label: t("menus.jobsdetail.partssublet"),
children: (
<JobsDetailPliContainer
job={job}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
/>
)
}
]}
/>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobDetailComponent);

View File

@@ -0,0 +1,103 @@
import { useQuery } from "@apollo/client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import SpinComponent from "../../components/loading-spinner/loading-spinner.component";
import NotFound from "../../components/not-found/not-found.component";
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import {
addRecentItem,
setBreadcrumbs,
setJobReadOnly,
setSelectedHeader
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateRecentItem } from "../../utils/create-recent-item";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import IsJobReadOnly from "../../utils/jobReadOnly";
import SimplifiedPartsJobsDetailComponent from "./simplified-parts-jobs-detail.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
addRecentItem: (item) => dispatch(addRecentItem(item)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
setJobReadOnly: (bool) => dispatch(setJobReadOnly(bool))
});
function SimplifiedPartsJobsDetailContainer({ setBreadcrumbs, addRecentItem, setSelectedHeader, setJobReadOnly }) {
const { jobId } = useParams();
const { t } = useTranslation();
const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
variables: { id: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
useEffect(() => {
setSelectedHeader("activejobs");
document.title = loading
? InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline")
})
: error
? InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline")
})
: t("titles.jobsdetail", {
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)"
}),
ro_number: (data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
});
setBreadcrumbs([
{ link: "/parts/jobs", label: t("titles.bc.jobs") },
{
link: `/parts/jobs/${jobId}`,
label: t("titles.bc.jobs-detail", {
number: (data && data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
})
}
]);
if (data && data.jobs_by_pk) {
setJobReadOnly(IsJobReadOnly(data.jobs_by_pk));
addRecentItem(
CreateRecentItem(
jobId,
"job",
`${data.jobs_by_pk.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(data.jobs_by_pk)}`,
`/manage/jobs/${jobId}`
)
);
}
}, [loading, data, t, error, setBreadcrumbs, jobId, addRecentItem, setSelectedHeader, setJobReadOnly]);
if (loading) return <SpinComponent />;
if (error) return <AlertComponent message={error.message} type="error" />;
if (!data.jobs_by_pk) return <NotFound />;
return data.jobs_by_pk ? (
<RbacWrapper action="jobs:detail">
<SimplifiedPartsJobsDetailComponent job={data.jobs_by_pk} refetch={refetch} />
</RbacWrapper>
) : (
<AlertComponent message={t("jobs.errors.noaccess")} type="error" />
);
}
export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobsDetailContainer);