feature/IO-3499-React-19 Checkpoint

This commit is contained in:
Dave
2026-01-21 17:55:57 -05:00
parent f5a618319a
commit 49bfb0849d
3 changed files with 226 additions and 154 deletions

View File

@@ -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}
/> />
); );
} }

View File

@@ -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} />
</> </>
); );

View File

@@ -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 />;