feature/IO-3499-React-19 Checkpoint
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client/react";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
@@ -14,7 +14,7 @@ import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
||||
import { DELETE_JOB, GET_JOB_WATCHERS, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
@@ -34,6 +34,8 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
|
||||
const EMPTY_ARRAY = Object.freeze([]);
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -123,6 +125,14 @@ export function JobsDetailHeaderActions({
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const jobId = job?.id;
|
||||
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]);
|
||||
|
||||
// Option A: coordinated Dropdown + Popconfirm open state so the menu doesn't unmount before Popconfirm renders.
|
||||
const [confirmKey, setConfirmKey] = useState(null);
|
||||
const confirmKeyRef = useRef(null);
|
||||
|
||||
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
|
||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||
const [deleteJob] = useMutation(DELETE_JOB);
|
||||
@@ -143,6 +153,15 @@ export function JobsDetailHeaderActions({
|
||||
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
||||
|
||||
const { data: jobWatchersData } = useQuery(GET_JOB_WATCHERS, {
|
||||
variables: watcherVars,
|
||||
skip: !jobId,
|
||||
fetchPolicy: "cache-first",
|
||||
notifyOnNetworkStatusChange: true
|
||||
});
|
||||
|
||||
const jobWatchersCount = jobWatchersData?.job_watchers?.length ?? job?.job_watchers?.length ?? 0;
|
||||
|
||||
const {
|
||||
treatments: { ImEXPay }
|
||||
} = useTreatmentsWithConfig({
|
||||
@@ -151,18 +170,85 @@ export function JobsDetailHeaderActions({
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const jobInProduction = useMemo(() => {
|
||||
return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
|
||||
}, [job, bodyshop.md_ro_statuses.production_statuses]);
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const productionStatuses = bodyshop?.md_ro_statuses?.production_statuses ?? EMPTY_ARRAY;
|
||||
const preProductionStatuses = bodyshop?.md_ro_statuses?.pre_production_statuses ?? EMPTY_ARRAY;
|
||||
const postProductionStatuses = bodyshop?.md_ro_statuses?.post_production_statuses ?? EMPTY_ARRAY;
|
||||
const jobStatus = job?.status;
|
||||
|
||||
const jobInPreProduction = useMemo(() => {
|
||||
return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
|
||||
}, [job.status, bodyshop.md_ro_statuses.pre_production_statuses]);
|
||||
const jobInProduction = productionStatuses.includes(jobStatus);
|
||||
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
|
||||
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
|
||||
|
||||
const jobInPostProduction = useMemo(() => {
|
||||
return bodyshop.md_ro_statuses.post_production_statuses.includes(job.status);
|
||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
||||
const openConfirm = useCallback((key) => {
|
||||
confirmKeyRef.current = key;
|
||||
setConfirmKey(key);
|
||||
setDropdownOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeConfirm = useCallback(() => {
|
||||
confirmKeyRef.current = null;
|
||||
setConfirmKey(null);
|
||||
}, []);
|
||||
|
||||
const handleDropdownOpenChange = useCallback(
|
||||
(nextOpen, info) => {
|
||||
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
|
||||
setDropdownOpen(nextOpen);
|
||||
if (!nextOpen) closeConfirm();
|
||||
},
|
||||
[closeConfirm]
|
||||
);
|
||||
|
||||
const renderPopconfirmMenuLabel = ({
|
||||
key,
|
||||
text,
|
||||
title,
|
||||
okText,
|
||||
cancelText,
|
||||
showCancel = true,
|
||||
closeDropdownOnConfirm = true,
|
||||
onConfirm
|
||||
}) => (
|
||||
<Popconfirm
|
||||
title={title}
|
||||
okText={okText}
|
||||
cancelText={cancelText}
|
||||
showCancel={showCancel}
|
||||
open={confirmKey === key}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen) openConfirm(key);
|
||||
else closeConfirm();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation?.();
|
||||
closeConfirm();
|
||||
|
||||
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close.
|
||||
if (closeDropdownOnConfirm) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
|
||||
onConfirm?.(e);
|
||||
}}
|
||||
onCancel={(e) => {
|
||||
e?.stopPropagation?.();
|
||||
closeConfirm();
|
||||
// Keep dropdown open on cancel so the user can continue using the menu.
|
||||
}}
|
||||
getPopupContainer={() => document.body}
|
||||
>
|
||||
<div
|
||||
style={{ width: "100%" }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openConfirm(key);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Popconfirm>
|
||||
);
|
||||
|
||||
// Function to show modal
|
||||
const showCancelScheduleModal = () => {
|
||||
@@ -549,12 +635,6 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
};
|
||||
|
||||
// Function to handle OK
|
||||
const handleCancelScheduleOK = async () => {
|
||||
await form.submit(); // Assuming 'form' is the Form instance from useForm()
|
||||
setIsCancelScheduleModalVisible(false);
|
||||
};
|
||||
|
||||
const handleLostSaleFinish = async ({ lost_sale_reason }) => {
|
||||
const jobUpdate = await cancelAllAppointments({
|
||||
variables: {
|
||||
@@ -884,34 +964,26 @@ export function JobsDetailHeaderActions({
|
||||
{
|
||||
key: "duplicate",
|
||||
id: "job-actions-duplicate",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleDuplicate}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicate")}
|
||||
</Popconfirm>
|
||||
)
|
||||
label: renderPopconfirmMenuLabel({
|
||||
key: "confirm-duplicate",
|
||||
text: t("menus.jobsactions.duplicate"),
|
||||
title: t("jobs.labels.duplicateconfirm"),
|
||||
okText: t("general.labels.yes"),
|
||||
cancelText: t("general.labels.no"),
|
||||
onConfirm: handleDuplicate
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "duplicatenolines",
|
||||
id: "job-actions-duplicatenolines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleDuplicateConfirm}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicatenolines")}
|
||||
</Popconfirm>
|
||||
)
|
||||
label: renderPopconfirmMenuLabel({
|
||||
key: "confirm-duplicate-nolines",
|
||||
text: t("menus.jobsactions.duplicatenolines"),
|
||||
title: t("jobs.labels.duplicateconfirm"),
|
||||
okText: t("general.labels.yes"),
|
||||
cancelText: t("general.labels.no"),
|
||||
onConfirm: handleDuplicateConfirm
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1085,21 +1157,25 @@ export function JobsDetailHeaderActions({
|
||||
key: "deletejob",
|
||||
id: "job-actions-deletejob",
|
||||
label:
|
||||
job.job_watchers.length === 0 ? (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleDeleteJob}
|
||||
>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
)
|
||||
jobWatchersCount === 0
|
||||
? renderPopconfirmMenuLabel({
|
||||
key: "confirm-deletejob",
|
||||
text: t("menus.jobsactions.deletejob"),
|
||||
title: t("jobs.labels.deleteconfirm"),
|
||||
okText: t("general.labels.yes"),
|
||||
cancelText: t("general.labels.no"),
|
||||
onConfirm: handleDeleteJob
|
||||
})
|
||||
: renderPopconfirmMenuLabel({
|
||||
key: "confirm-deletejob-watchers",
|
||||
text: t("menus.jobsactions.deletejob"),
|
||||
title: t("jobs.labels.deletewatchers"),
|
||||
showCancel: false,
|
||||
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly
|
||||
onConfirm: () => {
|
||||
// informational confirm only
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1118,15 +1194,14 @@ export function JobsDetailHeaderActions({
|
||||
id: "job-actions-voidjob",
|
||||
label: (
|
||||
<RbacWrapper action="jobs:void" noauth>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.voidjob")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={handleVoidJob}
|
||||
>
|
||||
{t("menus.jobsactions.void")}
|
||||
</Popconfirm>
|
||||
{renderPopconfirmMenuLabel({
|
||||
key: "confirm-voidjob",
|
||||
text: t("menus.jobsactions.void"),
|
||||
title: t("jobs.labels.voidjob"),
|
||||
okText: t("general.labels.yes"),
|
||||
cancelText: t("general.labels.no"),
|
||||
onConfirm: handleVoidJob
|
||||
})}
|
||||
</RbacWrapper>
|
||||
)
|
||||
});
|
||||
@@ -1163,20 +1238,12 @@ export function JobsDetailHeaderActions({
|
||||
<Modal
|
||||
title={t("menus.jobsactions.cancelallappointments")}
|
||||
open={isCancelScheduleModalVisible}
|
||||
onOk={handleCancelScheduleOK}
|
||||
onCancel={handleCancelScheduleModalCancel}
|
||||
footer={[
|
||||
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>,
|
||||
<Button
|
||||
form="cancelScheduleForm"
|
||||
htmlType="submit"
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleCancelScheduleOK}
|
||||
>
|
||||
<Button form="cancelScheduleForm" htmlType="submit" key="submit" type="primary" loading={loading}>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
]}
|
||||
@@ -1184,9 +1251,12 @@ export function JobsDetailHeaderActions({
|
||||
<Form
|
||||
layout="vertical"
|
||||
id="cancelScheduleForm"
|
||||
onFinish={(s) => {
|
||||
console.log(s);
|
||||
handleLostSaleFinish(s);
|
||||
onFinish={async (s) => {
|
||||
try {
|
||||
await handleLostSaleFinish(s);
|
||||
} finally {
|
||||
setIsCancelScheduleModalVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
@@ -1208,12 +1278,20 @@ export function JobsDetailHeaderActions({
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Dropdown menu={menu} trigger={["click"]} key="changestatus" open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={["click"]}
|
||||
key="changestatus"
|
||||
open={dropdownOpen}
|
||||
onOpenChange={handleDropdownOpenChange}
|
||||
>
|
||||
<Button>
|
||||
<span>{t("general.labels.actions")}</span>
|
||||
<DownCircleFilled />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Popover content={popOverContent} open={visibility} />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user