import { useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQuery } from "@apollo/client/react"; import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js"; import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx"; import { useIsEmployee } from "../../utils/useIsEmployee.js"; const EMPTY_ARRAY = Object.freeze([]); const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser }); function JobWatcherToggleContainer({ job, currentUser, bodyshop }) { const { treatments: { Enhanced_Payroll } } = useTreatmentsWithConfig({ attributes: {}, names: ["Enhanced_Payroll"], splitKey: bodyshop && bodyshop.imexshopid }); const isEmployee = useIsEmployee(bodyshop, currentUser); const [open, setOpen] = useState(false); const [selectedWatcher, setSelectedWatcher] = useState(null); const [selectedTeam, setSelectedTeam] = useState(null); const userEmail = currentUser?.email; const jobid = job?.id; const watcherVars = useMemo(() => ({ jobid }), [jobid]); const { data: watcherData, loading: watcherLoading, refetch } = useQuery(GET_JOB_WATCHERS, { variables: watcherVars, skip: !jobid, fetchPolicy: "cache-first", notifiyOnNetworkStatusChange: true }); // Refetch jobWatchers when the popover opens useEffect(() => { if (!jobid) return; if (open) { refetch().catch((err) => console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, { stack: err?.stack }) ); } }, [open, refetch, jobid]); // Do NOT clone arrays; keep referential stability for React Compiler and to reduce rerenders. 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, { onError: (err) => { if (err.graphQLErrors && err.graphQLErrors.length > 0) { const errorMessage = err.graphQLErrors[0].message; if ( errorMessage.includes("Uniqueness violation") || errorMessage.includes("idx_job_watchers_jobid_user_email_unique") ) { console.warn("Watcher already exists for this job and user."); // Only refetch for this edge case to ensure UI is accurate refetch().catch((e) => console.error( `Something went wrong fetching Notification Watchers after uniqueness violation: ${e?.message}`, { stack: e?.stack } ) ); } else { console.error(`Error adding job watcher: ${errorMessage}`); } } else { console.error(`Unexpected error adding job watcher: ${err.message || JSON.stringify(err)}`); } }, update(cache, { data }) { const inserted = data?.insert_job_watchers_one; if (!inserted) return; cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => { const prev = existing?.job_watchers ?? []; if (prev.some((w) => w.user_email === inserted.user_email)) return existing; return { ...existing, job_watchers: [...prev, inserted] }; }); } }); const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, { onError: (err) => console.error(`Error removing job watcher: ${err.message}`), update(cache, { data }) { const deleted = data?.delete_job_watchers?.returning?.[0]; if (!deleted?.user_email) return; cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => { const prev = existing?.job_watchers ?? []; return { ...existing, job_watchers: prev.filter((w) => w.user_email !== deleted.user_email) }; }); } }); const handleToggleSelf = useCallback(async () => { if (!jobid || !userEmail) return; if (adding || removing || !isEmployee) return; if (isWatching) { await removeWatcher({ variables: { jobid, userEmail } }); } else { await addWatcher({ variables: { jobid, userEmail } }); } }, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]); const handleRemoveWatcher = useCallback( async (email) => { if (!jobid) return; if (removing) return; if (!email) return; await removeWatcher({ variables: { jobid, userEmail: email } }); }, [removeWatcher, jobid, removing] ); const handleWatcherSelect = useCallback( async (selectedUser) => { if (!jobid) return; if (adding || removing) return; const employee = bodyshop?.employees?.find((e) => e.id === selectedUser); if (!employee?.user_email) return; const email = employee.user_email; const isAlreadyWatching = watcherEmailSet.has(email); if (isAlreadyWatching) { await handleRemoveWatcher(email); } else { await addWatcher({ variables: { jobid, userEmail: email } }); } setSelectedWatcher(null); }, [watcherEmailSet, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing] ); const handleTeamSelect = useCallback( async (team) => { if (!jobid) return; if (adding) return; 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) { console.warn("All selected team members are already watchers."); setSelectedTeam(null); return; } await Promise.all( newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } }) ) ); setSelectedTeam(null); }, [watcherEmailSet, addWatcher, jobid, adding] ); return ( ); } export default connect(mapStateToProps)(JobWatcherToggleContainer);