feature/IO-3499-React-19 Checkpoint
This commit is contained in:
@@ -8,6 +8,8 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
|||||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
|
const EMPTY_ARRAY = Object.freeze([]);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
@@ -27,21 +29,24 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||||
|
|
||||||
const userEmail = currentUser.email;
|
const userEmail = currentUser?.email;
|
||||||
const jobid = job.id;
|
const jobid = job?.id;
|
||||||
|
const watcherVars = useMemo(() => ({ jobid }), [jobid]);
|
||||||
|
|
||||||
// Fetch current watchers with refetch capability
|
|
||||||
const {
|
const {
|
||||||
data: watcherData,
|
data: watcherData,
|
||||||
loading: watcherLoading,
|
loading: watcherLoading,
|
||||||
refetch
|
refetch
|
||||||
} = useQuery(GET_JOB_WATCHERS, {
|
} = useQuery(GET_JOB_WATCHERS, {
|
||||||
variables: { jobid },
|
variables: watcherVars,
|
||||||
fetchPolicy: "cache-and-network" // Ensure fresh data from server
|
skip: !jobid,
|
||||||
|
fetchPolicy: "cache-first",
|
||||||
|
notifiyOnNetworkStatusChange: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refetch jobWatchers when the popover opens (open changes to true)
|
// Refetch jobWatchers when the popover opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!jobid) return;
|
||||||
if (open) {
|
if (open) {
|
||||||
refetch().catch((err) =>
|
refetch().catch((err) =>
|
||||||
console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, {
|
console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, {
|
||||||
@@ -49,18 +54,17 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [open, refetch]);
|
}, [open, refetch, jobid]);
|
||||||
|
|
||||||
const jobWatchers = useMemo(() => (watcherData?.job_watchers ? [...watcherData.job_watchers] : []), [watcherData]);
|
// Do NOT clone arrays; keep referential stability for React Compiler and to reduce rerenders.
|
||||||
const isWatching = jobWatchers.some((w) => w.user_email === userEmail);
|
const jobWatchers = watcherData?.job_watchers ?? EMPTY_ARRAY;
|
||||||
|
const watcherEmailSet = useMemo(
|
||||||
|
() => new Set((jobWatchers ?? EMPTY_ARRAY).map((w) => w?.user_email).filter(Boolean)),
|
||||||
|
[jobWatchers]
|
||||||
|
);
|
||||||
|
const isWatching = !!userEmail && watcherEmailSet.has(userEmail);
|
||||||
|
|
||||||
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
||||||
onCompleted: () =>
|
|
||||||
refetch().catch((err) =>
|
|
||||||
console.error(`Something went wrong fetching Notification Watchers after add: ${err?.message}`, {
|
|
||||||
stack: err?.stack
|
|
||||||
})
|
|
||||||
),
|
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
if (err.graphQLErrors && err.graphQLErrors.length > 0) {
|
if (err.graphQLErrors && err.graphQLErrors.length > 0) {
|
||||||
const errorMessage = err.graphQLErrors[0].message;
|
const errorMessage = err.graphQLErrors[0].message;
|
||||||
@@ -69,12 +73,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
errorMessage.includes("idx_job_watchers_jobid_user_email_unique")
|
errorMessage.includes("idx_job_watchers_jobid_user_email_unique")
|
||||||
) {
|
) {
|
||||||
console.warn("Watcher already exists for this job and user.");
|
console.warn("Watcher already exists for this job and user.");
|
||||||
refetch().catch((err) =>
|
// Only refetch for this edge case to ensure UI is accurate
|
||||||
|
refetch().catch((e) =>
|
||||||
console.error(
|
console.error(
|
||||||
`Something went wrong fetching Notification Watchers after uniqueness violation: ${err?.message}`,
|
`Something went wrong fetching Notification Watchers after uniqueness violation: ${e?.message}`,
|
||||||
{ stack: err?.stack }
|
{ stack: e?.stack }
|
||||||
)
|
)
|
||||||
); // Sync with server to ensure UI reflects actual state
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error adding job watcher: ${errorMessage}`);
|
console.error(`Error adding job watcher: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
@@ -83,65 +88,41 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
update(cache, { data }) {
|
update(cache, { data }) {
|
||||||
if (!data || !data.insert_job_watchers_one) {
|
const inserted = data?.insert_job_watchers_one;
|
||||||
console.warn("No data or insert_job_watchers_one returned from mutation, skipping cache update.");
|
if (!inserted) return;
|
||||||
refetch().catch((err) =>
|
|
||||||
console.error(`Something went wrong updating Notification Watchers after add: ${err?.message}`, {
|
|
||||||
stack: err?.stack
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const insert_job_watchers_one = data.insert_job_watchers_one;
|
cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => {
|
||||||
const existingData = cache.readQuery({
|
const prev = existing?.job_watchers ?? [];
|
||||||
query: GET_JOB_WATCHERS,
|
if (prev.some((w) => w.user_email === inserted.user_email)) return existing;
|
||||||
variables: { jobid }
|
|
||||||
});
|
|
||||||
|
|
||||||
cache.writeQuery({
|
return {
|
||||||
query: GET_JOB_WATCHERS,
|
...existing,
|
||||||
variables: { jobid },
|
job_watchers: [...prev, inserted]
|
||||||
data: {
|
};
|
||||||
...existingData,
|
|
||||||
job_watchers: [...(existingData?.job_watchers || []), insert_job_watchers_one]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
||||||
onCompleted: () =>
|
|
||||||
refetch().catch((err) =>
|
|
||||||
console.error(`Something went wrong fetching Notification Watchers after remove: ${err?.message}`, {
|
|
||||||
stack: err?.stack
|
|
||||||
})
|
|
||||||
), // Refetch to sync with server after success
|
|
||||||
onError: (err) => console.error(`Error removing job watcher: ${err.message}`),
|
onError: (err) => console.error(`Error removing job watcher: ${err.message}`),
|
||||||
update(cache, { data: { delete_job_watchers } }) {
|
update(cache, { data }) {
|
||||||
const existingData = cache.readQuery({
|
const deleted = data?.delete_job_watchers?.returning?.[0];
|
||||||
query: GET_JOB_WATCHERS,
|
if (!deleted?.user_email) return;
|
||||||
variables: { jobid }
|
|
||||||
});
|
|
||||||
|
|
||||||
const deletedWatcher = delete_job_watchers.returning[0];
|
cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => {
|
||||||
const updatedWatchers = deletedWatcher
|
const prev = existing?.job_watchers ?? [];
|
||||||
? (existingData?.job_watchers || []).filter((watcher) => watcher.user_email !== deletedWatcher.user_email)
|
return {
|
||||||
: existingData?.job_watchers || [];
|
...existing,
|
||||||
|
job_watchers: prev.filter((w) => w.user_email !== deleted.user_email)
|
||||||
cache.writeQuery({
|
};
|
||||||
query: GET_JOB_WATCHERS,
|
|
||||||
variables: { jobid },
|
|
||||||
data: {
|
|
||||||
...existingData,
|
|
||||||
job_watchers: updatedWatchers
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleSelf = useCallback(async () => {
|
const handleToggleSelf = useCallback(async () => {
|
||||||
|
if (!jobid || !userEmail) return;
|
||||||
if (adding || removing || !isEmployee) return;
|
if (adding || removing || !isEmployee) return;
|
||||||
|
|
||||||
if (isWatching) {
|
if (isWatching) {
|
||||||
await removeWatcher({ variables: { jobid, userEmail } });
|
await removeWatcher({ variables: { jobid, userEmail } });
|
||||||
} else {
|
} else {
|
||||||
@@ -151,7 +132,9 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
|
|
||||||
const handleRemoveWatcher = useCallback(
|
const handleRemoveWatcher = useCallback(
|
||||||
async (email) => {
|
async (email) => {
|
||||||
|
if (!jobid) return;
|
||||||
if (removing) return;
|
if (removing) return;
|
||||||
|
if (!email) return;
|
||||||
await removeWatcher({ variables: { jobid, userEmail: email } });
|
await removeWatcher({ variables: { jobid, userEmail: email } });
|
||||||
},
|
},
|
||||||
[removeWatcher, jobid, removing]
|
[removeWatcher, jobid, removing]
|
||||||
@@ -159,36 +142,45 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
|
|
||||||
const handleWatcherSelect = useCallback(
|
const handleWatcherSelect = useCallback(
|
||||||
async (selectedUser) => {
|
async (selectedUser) => {
|
||||||
|
if (!jobid) return;
|
||||||
if (adding || removing) return;
|
if (adding || removing) return;
|
||||||
const employee = bodyshop.employees.find((e) => e.id === selectedUser);
|
|
||||||
if (!employee) return;
|
const employee = bodyshop?.employees?.find((e) => e.id === selectedUser);
|
||||||
|
if (!employee?.user_email) return;
|
||||||
|
|
||||||
const email = employee.user_email;
|
const email = employee.user_email;
|
||||||
const isAlreadyWatching = jobWatchers.some((w) => w.user_email === email);
|
const isAlreadyWatching = watcherEmailSet.has(email);
|
||||||
|
|
||||||
if (isAlreadyWatching) {
|
if (isAlreadyWatching) {
|
||||||
await handleRemoveWatcher(email);
|
await handleRemoveWatcher(email);
|
||||||
} else {
|
} else {
|
||||||
await addWatcher({ variables: { jobid, userEmail: email } });
|
await addWatcher({ variables: { jobid, userEmail: email } });
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedWatcher(null);
|
setSelectedWatcher(null);
|
||||||
},
|
},
|
||||||
[jobWatchers, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing]
|
[watcherEmailSet, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTeamSelect = useCallback(
|
const handleTeamSelect = useCallback(
|
||||||
async (team) => {
|
async (team) => {
|
||||||
|
if (!jobid) return;
|
||||||
if (adding) return;
|
if (adding) return;
|
||||||
const selectedTeamMembers = JSON.parse(team);
|
|
||||||
const newWatchers = selectedTeamMembers.filter(
|
|
||||||
(email) => !jobWatchers.some((watcher) => watcher.user_email === email)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let selectedTeamMembers = [];
|
||||||
|
try {
|
||||||
|
selectedTeamMembers = Array.isArray(team) ? team : JSON.parse(team);
|
||||||
|
} catch {
|
||||||
|
selectedTeamMembers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWatchers = (selectedTeamMembers ?? []).filter(Boolean).filter((email) => !watcherEmailSet.has(email));
|
||||||
if (newWatchers.length === 0) {
|
if (newWatchers.length === 0) {
|
||||||
console.warn("All selected team members are already watchers.");
|
console.warn("All selected team members are already watchers.");
|
||||||
setSelectedTeam(null);
|
setSelectedTeam(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
newWatchers.map((email) =>
|
newWatchers.map((email) =>
|
||||||
addWatcher({
|
addWatcher({
|
||||||
@@ -199,8 +191,10 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setSelectedTeam(null);
|
||||||
},
|
},
|
||||||
[jobWatchers, addWatcher, jobid, adding]
|
[watcherEmailSet, addWatcher, jobid, adding]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -223,7 +217,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
handleWatcherSelect={handleWatcherSelect}
|
handleWatcherSelect={handleWatcherSelect}
|
||||||
handleTeamSelect={handleTeamSelect}
|
handleTeamSelect={handleTeamSelect}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
isEmployee={isEmployee} // Pass isEmployee to the component
|
isEmployee={isEmployee}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
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 { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import { useMemo, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
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 { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
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 { 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 { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
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 DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
|
|
||||||
|
const EMPTY_ARRAY = Object.freeze([]);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -123,6 +125,14 @@ export function JobsDetailHeaderActions({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dropdownOpen, setDropdownOpen] = 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 [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
|
||||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||||
const [deleteJob] = useMutation(DELETE_JOB);
|
const [deleteJob] = useMutation(DELETE_JOB);
|
||||||
@@ -143,6 +153,15 @@ export function JobsDetailHeaderActions({
|
|||||||
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||||
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
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 {
|
const {
|
||||||
treatments: { ImEXPay }
|
treatments: { ImEXPay }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -151,18 +170,85 @@ export function JobsDetailHeaderActions({
|
|||||||
splitKey: bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobInProduction = useMemo(() => {
|
const productionStatuses = bodyshop?.md_ro_statuses?.production_statuses ?? EMPTY_ARRAY;
|
||||||
return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
|
const preProductionStatuses = bodyshop?.md_ro_statuses?.pre_production_statuses ?? EMPTY_ARRAY;
|
||||||
}, [job, bodyshop.md_ro_statuses.production_statuses]);
|
const postProductionStatuses = bodyshop?.md_ro_statuses?.post_production_statuses ?? EMPTY_ARRAY;
|
||||||
const [visibility, setVisibility] = useState(false);
|
const jobStatus = job?.status;
|
||||||
|
|
||||||
const jobInPreProduction = useMemo(() => {
|
const jobInProduction = productionStatuses.includes(jobStatus);
|
||||||
return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
|
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
|
||||||
}, [job.status, bodyshop.md_ro_statuses.pre_production_statuses]);
|
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
|
||||||
|
|
||||||
const jobInPostProduction = useMemo(() => {
|
const openConfirm = useCallback((key) => {
|
||||||
return bodyshop.md_ro_statuses.post_production_statuses.includes(job.status);
|
confirmKeyRef.current = key;
|
||||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
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
|
// Function to show modal
|
||||||
const showCancelScheduleModal = () => {
|
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 handleLostSaleFinish = async ({ lost_sale_reason }) => {
|
||||||
const jobUpdate = await cancelAllAppointments({
|
const jobUpdate = await cancelAllAppointments({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -884,34 +964,26 @@ export function JobsDetailHeaderActions({
|
|||||||
{
|
{
|
||||||
key: "duplicate",
|
key: "duplicate",
|
||||||
id: "job-actions-duplicate",
|
id: "job-actions-duplicate",
|
||||||
label: (
|
label: renderPopconfirmMenuLabel({
|
||||||
<Popconfirm
|
key: "confirm-duplicate",
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
text: t("menus.jobsactions.duplicate"),
|
||||||
okText="Yes"
|
title: t("jobs.labels.duplicateconfirm"),
|
||||||
cancelText="No"
|
okText: t("general.labels.yes"),
|
||||||
onClick={(e) => e.stopPropagation()}
|
cancelText: t("general.labels.no"),
|
||||||
onConfirm={handleDuplicate}
|
onConfirm: handleDuplicate
|
||||||
getPopupContainer={(trigger) => trigger.parentNode}
|
})
|
||||||
>
|
|
||||||
{t("menus.jobsactions.duplicate")}
|
|
||||||
</Popconfirm>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "duplicatenolines",
|
key: "duplicatenolines",
|
||||||
id: "job-actions-duplicatenolines",
|
id: "job-actions-duplicatenolines",
|
||||||
label: (
|
label: renderPopconfirmMenuLabel({
|
||||||
<Popconfirm
|
key: "confirm-duplicate-nolines",
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
text: t("menus.jobsactions.duplicatenolines"),
|
||||||
okText="Yes"
|
title: t("jobs.labels.duplicateconfirm"),
|
||||||
cancelText="No"
|
okText: t("general.labels.yes"),
|
||||||
onClick={(e) => e.stopPropagation()}
|
cancelText: t("general.labels.no"),
|
||||||
onConfirm={handleDuplicateConfirm}
|
onConfirm: handleDuplicateConfirm
|
||||||
getPopupContainer={(trigger) => trigger.parentNode}
|
})
|
||||||
>
|
|
||||||
{t("menus.jobsactions.duplicatenolines")}
|
|
||||||
</Popconfirm>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1085,21 +1157,25 @@ export function JobsDetailHeaderActions({
|
|||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
id: "job-actions-deletejob",
|
id: "job-actions-deletejob",
|
||||||
label:
|
label:
|
||||||
job.job_watchers.length === 0 ? (
|
jobWatchersCount === 0
|
||||||
<Popconfirm
|
? renderPopconfirmMenuLabel({
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
key: "confirm-deletejob",
|
||||||
okText={t("general.labels.yes")}
|
text: t("menus.jobsactions.deletejob"),
|
||||||
cancelText={t("general.labels.no")}
|
title: t("jobs.labels.deleteconfirm"),
|
||||||
onClick={(e) => e.stopPropagation()}
|
okText: t("general.labels.yes"),
|
||||||
onConfirm={handleDeleteJob}
|
cancelText: t("general.labels.no"),
|
||||||
>
|
onConfirm: handleDeleteJob
|
||||||
{t("menus.jobsactions.deletejob")}
|
})
|
||||||
</Popconfirm>
|
: renderPopconfirmMenuLabel({
|
||||||
) : (
|
key: "confirm-deletejob-watchers",
|
||||||
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
text: t("menus.jobsactions.deletejob"),
|
||||||
{t("menus.jobsactions.deletejob")}
|
title: t("jobs.labels.deletewatchers"),
|
||||||
</Popconfirm>
|
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",
|
id: "job-actions-voidjob",
|
||||||
label: (
|
label: (
|
||||||
<RbacWrapper action="jobs:void" noauth>
|
<RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
{renderPopconfirmMenuLabel({
|
||||||
title={t("jobs.labels.voidjob")}
|
key: "confirm-voidjob",
|
||||||
okText={t("general.labels.yes")}
|
text: t("menus.jobsactions.void"),
|
||||||
cancelText={t("general.labels.no")}
|
title: t("jobs.labels.voidjob"),
|
||||||
onClick={(e) => e.stopPropagation()}
|
okText: t("general.labels.yes"),
|
||||||
onConfirm={handleVoidJob}
|
cancelText: t("general.labels.no"),
|
||||||
>
|
onConfirm: handleVoidJob
|
||||||
{t("menus.jobsactions.void")}
|
})}
|
||||||
</Popconfirm>
|
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1163,20 +1238,12 @@ export function JobsDetailHeaderActions({
|
|||||||
<Modal
|
<Modal
|
||||||
title={t("menus.jobsactions.cancelallappointments")}
|
title={t("menus.jobsactions.cancelallappointments")}
|
||||||
open={isCancelScheduleModalVisible}
|
open={isCancelScheduleModalVisible}
|
||||||
onOk={handleCancelScheduleOK}
|
|
||||||
onCancel={handleCancelScheduleModalCancel}
|
onCancel={handleCancelScheduleModalCancel}
|
||||||
footer={[
|
footer={[
|
||||||
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
|
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
|
||||||
{t("general.actions.cancel")}
|
{t("general.actions.cancel")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button form="cancelScheduleForm" htmlType="submit" key="submit" type="primary" loading={loading}>
|
||||||
form="cancelScheduleForm"
|
|
||||||
htmlType="submit"
|
|
||||||
key="submit"
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={handleCancelScheduleOK}
|
|
||||||
>
|
|
||||||
{t("appointments.actions.cancel")}
|
{t("appointments.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
@@ -1184,9 +1251,12 @@ export function JobsDetailHeaderActions({
|
|||||||
<Form
|
<Form
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
id="cancelScheduleForm"
|
id="cancelScheduleForm"
|
||||||
onFinish={(s) => {
|
onFinish={async (s) => {
|
||||||
console.log(s);
|
try {
|
||||||
handleLostSaleFinish(s);
|
await handleLostSaleFinish(s);
|
||||||
|
} finally {
|
||||||
|
setIsCancelScheduleModalVisible(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -1208,12 +1278,20 @@ export function JobsDetailHeaderActions({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Dropdown menu={menu} trigger={["click"]} key="changestatus" open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
|
||||||
|
<Dropdown
|
||||||
|
menu={menu}
|
||||||
|
trigger={["click"]}
|
||||||
|
key="changestatus"
|
||||||
|
open={dropdownOpen}
|
||||||
|
onOpenChange={handleDropdownOpenChange}
|
||||||
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<span>{t("general.labels.actions")}</span>
|
<span>{t("general.labels.actions")}</span>
|
||||||
<DownCircleFilled />
|
<DownCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Popover content={popOverContent} open={visibility} />
|
<Popover content={popOverContent} open={visibility} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function JobsDocumentsContainer({
|
|||||||
variables: { jobId: jobId },
|
variables: { jobId: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !!billId
|
skip: !jobId || !!billId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|||||||
Reference in New Issue
Block a user