IO-3166-Global-Notifications-Part-2 - Checkpoint
This commit is contained in:
@@ -656,7 +656,7 @@ function Header({
|
|||||||
icon: unreadLoading ? (
|
icon: unreadLoading ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Badge count={unreadCount}>
|
<Badge size="small" count={unreadCount}>
|
||||||
<BellFilled />
|
<BellFilled />
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
|
import { Avatar, Button, Divider, List, Popover, Select, Tooltip, Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmployeeSearchSelectComponent from "../../components/employee-search-select/employee-search-select.component.jsx";
|
||||||
|
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export default function JobWatcherToggleComponent({
|
||||||
|
jobWatchers,
|
||||||
|
isWatching,
|
||||||
|
watcherLoading,
|
||||||
|
adding,
|
||||||
|
removing,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
selectedWatcher,
|
||||||
|
setSelectedWatcher,
|
||||||
|
selectedTeam,
|
||||||
|
bodyshop,
|
||||||
|
Enhanced_Payroll,
|
||||||
|
handleToggleSelf,
|
||||||
|
handleRemoveWatcher,
|
||||||
|
handleWatcherSelect,
|
||||||
|
handleTeamSelect
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleRenderItem = (watcher) => {
|
||||||
|
// Check if watcher is defined and has user_email
|
||||||
|
if (!watcher || !watcher.user_email) {
|
||||||
|
return null; // Skip rendering invalid watchers
|
||||||
|
}
|
||||||
|
|
||||||
|
const employee = bodyshop?.employees?.find((e) => e.user_email === watcher.user_email);
|
||||||
|
const displayName = employee ? `${employee.first_name} ${employee.last_name}` : watcher.user_email;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleRemoveWatcher(watcher.user_email)}
|
||||||
|
disabled={adding || removing} // Optional: Disable button during mutations
|
||||||
|
>
|
||||||
|
{t("notifications.actions.remove")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<Avatar icon={<UserOutlined />} />}
|
||||||
|
title={<Text>{displayName}</Text>}
|
||||||
|
description={watcher.user_email}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popoverContent = (
|
||||||
|
<div style={{ width: 600 }}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type={isWatching ? "primary" : "default"}
|
||||||
|
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||||
|
onClick={handleToggleSelf}
|
||||||
|
loading={adding || removing}
|
||||||
|
>
|
||||||
|
{isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}
|
||||||
|
</Button>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||||
|
{t("notifications.labels.watching-issue")}
|
||||||
|
</Text>
|
||||||
|
{watcherLoading ? (
|
||||||
|
<LoadingSpinner />
|
||||||
|
) : jobWatchers && jobWatchers.length > 0 ? (
|
||||||
|
<List dataSource={jobWatchers} renderItem={handleRenderItem} />
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">{t("notifications.labels.no-watchers")}</Text>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Text type="secondary">{t("notifications.labels.add-watchers")}</Text>
|
||||||
|
<EmployeeSearchSelectComponent
|
||||||
|
style={{ minWidth: "100%" }}
|
||||||
|
options={bodyshop?.employees?.filter((e) => jobWatchers.every((w) => w.user_email !== e.user_email)) || []}
|
||||||
|
placeholder={t("notifications.labels.employee-search")}
|
||||||
|
value={selectedWatcher}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedWatcher(value);
|
||||||
|
handleWatcherSelect(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{Enhanced_Payroll && bodyshop?.employee_teams?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Text type="secondary">{t("notifications.labels.add-watchers-team")}</Text>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{ minWidth: "100%" }}
|
||||||
|
placeholder={t("notifications.labels.teams-search")}
|
||||||
|
value={selectedTeam}
|
||||||
|
onChange={handleTeamSelect}
|
||||||
|
options={
|
||||||
|
bodyshop?.employee_teams?.map((team) => {
|
||||||
|
const teamMembers = team.employee_team_members
|
||||||
|
.map((member) => {
|
||||||
|
const employee = bodyshop?.employees?.find((e) => e.id === member.employeeid);
|
||||||
|
return employee ? employee.user_email : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: JSON.stringify(teamMembers),
|
||||||
|
label: team.name
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
|
||||||
|
<Tooltip title={isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
type={isWatching ? "primary" : "default"}
|
||||||
|
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
||||||
|
loading={watcherLoading}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
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 { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
||||||
|
const {
|
||||||
|
treatments: { Enhanced_Payroll }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Enhanced_Payroll"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
|
const userEmail = currentUser.email;
|
||||||
|
const jobid = job.id;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||||
|
|
||||||
|
// Fetch current watchers with refetch capability
|
||||||
|
const {
|
||||||
|
data: watcherData,
|
||||||
|
loading: watcherLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery(GET_JOB_WATCHERS, {
|
||||||
|
variables: { jobid },
|
||||||
|
fetchPolicy: "cache-and-network" // Ensure fresh data from server
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refetch jobWatchers when the popover opens (open changes to true)
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
refetch().catch((err) =>
|
||||||
|
console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, {
|
||||||
|
stack: err?.stack
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [open, refetch]);
|
||||||
|
|
||||||
|
const jobWatchers = useMemo(() => (watcherData?.job_watchers ? [...watcherData.job_watchers] : []), [watcherData]);
|
||||||
|
const isWatching = jobWatchers.some((w) => w.user_email === userEmail);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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.");
|
||||||
|
refetch().catch((err) =>
|
||||||
|
console.error(
|
||||||
|
`Something went wrong fetching Notification Watchers after uniqueness violation: ${err?.message}`,
|
||||||
|
{ stack: err?.stack }
|
||||||
|
)
|
||||||
|
); // Sync with server to ensure UI reflects actual state
|
||||||
|
} else {
|
||||||
|
console.error(`Error adding job watcher: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Unexpected error adding job watcher: ${err.message || JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update(cache, { data }) {
|
||||||
|
if (!data || !data.insert_job_watchers_one) {
|
||||||
|
console.warn("No data or insert_job_watchers_one returned from mutation, skipping cache update.");
|
||||||
|
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;
|
||||||
|
const existingData = cache.readQuery({
|
||||||
|
query: GET_JOB_WATCHERS,
|
||||||
|
variables: { jobid }
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: GET_JOB_WATCHERS,
|
||||||
|
variables: { jobid },
|
||||||
|
data: {
|
||||||
|
...existingData,
|
||||||
|
job_watchers: [...(existingData?.job_watchers || []), insert_job_watchers_one]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`),
|
||||||
|
update(cache, { data: { delete_job_watchers } }) {
|
||||||
|
const existingData = cache.readQuery({
|
||||||
|
query: GET_JOB_WATCHERS,
|
||||||
|
variables: { jobid }
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedWatcher = delete_job_watchers.returning[0];
|
||||||
|
const updatedWatchers = deletedWatcher
|
||||||
|
? (existingData?.job_watchers || []).filter((watcher) => watcher.user_email !== deletedWatcher.user_email)
|
||||||
|
: existingData?.job_watchers || [];
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: GET_JOB_WATCHERS,
|
||||||
|
variables: { jobid },
|
||||||
|
data: {
|
||||||
|
...existingData,
|
||||||
|
job_watchers: updatedWatchers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleSelf = useCallback(async () => {
|
||||||
|
if (adding || removing) return;
|
||||||
|
if (isWatching) {
|
||||||
|
await removeWatcher({ variables: { jobid, userEmail } });
|
||||||
|
} else {
|
||||||
|
await addWatcher({ variables: { jobid, userEmail } });
|
||||||
|
}
|
||||||
|
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
||||||
|
|
||||||
|
const handleRemoveWatcher = useCallback(
|
||||||
|
async (email) => {
|
||||||
|
if (removing) return;
|
||||||
|
await removeWatcher({ variables: { jobid, userEmail: email } });
|
||||||
|
},
|
||||||
|
[removeWatcher, jobid, removing]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWatcherSelect = useCallback(
|
||||||
|
async (selectedUser) => {
|
||||||
|
if (adding || removing) return;
|
||||||
|
const employee = bodyshop.employees.find((e) => e.id === selectedUser);
|
||||||
|
if (!employee) return;
|
||||||
|
|
||||||
|
const email = employee.user_email;
|
||||||
|
const isAlreadyWatching = jobWatchers.some((w) => w.user_email === email);
|
||||||
|
|
||||||
|
if (isAlreadyWatching) {
|
||||||
|
await handleRemoveWatcher(email);
|
||||||
|
} else {
|
||||||
|
await addWatcher({ variables: { jobid, userEmail: email } });
|
||||||
|
}
|
||||||
|
setSelectedWatcher(null);
|
||||||
|
},
|
||||||
|
[jobWatchers, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTeamSelect = useCallback(
|
||||||
|
async (team) => {
|
||||||
|
if (adding) return;
|
||||||
|
const selectedTeamMembers = JSON.parse(team);
|
||||||
|
const newWatchers = selectedTeamMembers.filter(
|
||||||
|
(email) => !jobWatchers.some((watcher) => watcher.user_email === 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 } })));
|
||||||
|
},
|
||||||
|
[jobWatchers, addWatcher, jobid, adding]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JobWatcherToggleComponent
|
||||||
|
jobWatchers={jobWatchers}
|
||||||
|
isWatching={isWatching}
|
||||||
|
watcherLoading={watcherLoading}
|
||||||
|
adding={adding}
|
||||||
|
removing={removing}
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
selectedWatcher={selectedWatcher}
|
||||||
|
setSelectedWatcher={setSelectedWatcher}
|
||||||
|
selectedTeam={selectedTeam}
|
||||||
|
setSelectedTeam={setSelectedTeam}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
Enhanced_Payroll={Enhanced_Payroll}
|
||||||
|
handleToggleSelf={handleToggleSelf}
|
||||||
|
handleRemoveWatcher={handleRemoveWatcher}
|
||||||
|
handleWatcherSelect={handleWatcherSelect}
|
||||||
|
handleTeamSelect={handleTeamSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(JobWatcherToggleContainer);
|
||||||
@@ -45,7 +45,11 @@ const NotificationCenterComponent = ({
|
|||||||
>
|
>
|
||||||
{t("notifications.labels.ro-number", { ro_number: notification.roNumber })}
|
{t("notifications.labels.ro-number", { ro_number: notification.roNumber })}
|
||||||
</Link>
|
</Link>
|
||||||
<Text type="secondary" className="relative-time">
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
className="relative-time"
|
||||||
|
title={day(notification.created_at).format("YYYY-MM-DD hh:mm A")}
|
||||||
|
>
|
||||||
{day(notification.created_at).fromNow()}
|
{day(notification.created_at).fromNow()}
|
||||||
</Text>
|
</Text>
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
|
||||||
import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
|
||||||
import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
|
|
||||||
import { Avatar, Button, Divider, List, Popover, Select, Tooltip, Typography } from "antd";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
|
||||||
import EmployeeSearchSelectComponent from "../../components/employee-search-select/employee-search-select.component.jsx";
|
|
||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx";
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
currentUser: selectCurrentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
const JobWatcherToggle = ({ job, currentUser, bodyshop }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
treatments: { Enhanced_Payroll }
|
|
||||||
} = useSplitTreatments({
|
|
||||||
attributes: {},
|
|
||||||
names: ["Enhanced_Payroll"],
|
|
||||||
splitKey: bodyshop && bodyshop.imexshopid
|
|
||||||
});
|
|
||||||
|
|
||||||
const userEmail = currentUser.email;
|
|
||||||
const jobid = job.id;
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
|
||||||
|
|
||||||
// Fetch current watchers
|
|
||||||
const { data: watcherData, loading: watcherLoading } = useQuery(GET_JOB_WATCHERS, { variables: { jobid } });
|
|
||||||
|
|
||||||
// Extract watchers list
|
|
||||||
const jobWatchers = useMemo(() => watcherData?.job_watchers || [], [watcherData]);
|
|
||||||
|
|
||||||
const isWatching = jobWatchers.some((w) => w.user_email === userEmail);
|
|
||||||
|
|
||||||
// Add watcher mutation with cache update
|
|
||||||
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
|
||||||
update(cache, { data: { insert_job_watchers_one } }) {
|
|
||||||
const existingData = cache.readQuery({
|
|
||||||
query: GET_JOB_WATCHERS,
|
|
||||||
variables: { jobid }
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedWatchers = [...(existingData?.job_watchers || []), insert_job_watchers_one];
|
|
||||||
|
|
||||||
cache.writeQuery({
|
|
||||||
query: GET_JOB_WATCHERS,
|
|
||||||
variables: { jobid },
|
|
||||||
data: {
|
|
||||||
...existingData,
|
|
||||||
job_watchers: updatedWatchers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove watcher mutation with cache update
|
|
||||||
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
|
||||||
update(cache, { data: { delete_job_watchers } }) {
|
|
||||||
const existingData = cache.readQuery({
|
|
||||||
query: GET_JOB_WATCHERS,
|
|
||||||
variables: { jobid }
|
|
||||||
});
|
|
||||||
|
|
||||||
const deletedWatcher = delete_job_watchers.returning[0]; // Safely assume one row deleted
|
|
||||||
const updatedWatchers = deletedWatcher
|
|
||||||
? (existingData?.job_watchers || []).filter((watcher) => watcher.user_email !== deletedWatcher.user_email)
|
|
||||||
: existingData?.job_watchers || []; // No change if nothing deleted
|
|
||||||
|
|
||||||
cache.writeQuery({
|
|
||||||
query: GET_JOB_WATCHERS,
|
|
||||||
variables: { jobid },
|
|
||||||
data: {
|
|
||||||
...existingData,
|
|
||||||
job_watchers: updatedWatchers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toggle watcher for self
|
|
||||||
const handleToggleSelf = useCallback(() => {
|
|
||||||
(isWatching
|
|
||||||
? removeWatcher({ variables: { jobid, userEmail } })
|
|
||||||
: addWatcher({ variables: { jobid, userEmail } })
|
|
||||||
).catch((err) => console.error(`Error updating job watcher: ${err.message}`));
|
|
||||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail]);
|
|
||||||
|
|
||||||
// Handle removing a watcher
|
|
||||||
const handleRemoveWatcher = (userEmail) => {
|
|
||||||
removeWatcher({ variables: { jobid, userEmail } }).catch((err) =>
|
|
||||||
console.error(`Error removing job watcher: ${err.message}`)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWatcherSelect = (selectedUser) => {
|
|
||||||
const employee = bodyshop.employees.find((e) => e.id === selectedUser);
|
|
||||||
if (!employee) return;
|
|
||||||
|
|
||||||
const isAlreadyWatching = jobWatchers.some((w) => w.user_email === employee.user_email);
|
|
||||||
|
|
||||||
if (isAlreadyWatching) {
|
|
||||||
handleRemoveWatcher(employee.user_email);
|
|
||||||
} else {
|
|
||||||
addWatcher({ variables: { jobid, userEmail: employee.user_email } }).catch((err) =>
|
|
||||||
console.error(`Error adding job watcher: ${err.message}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedWatcher(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTeamSelect = (team) => {
|
|
||||||
const selectedTeamMembers = JSON.parse(team);
|
|
||||||
|
|
||||||
const newWatchers = selectedTeamMembers.filter(
|
|
||||||
(email) => !jobWatchers.some((watcher) => watcher.user_email === email)
|
|
||||||
);
|
|
||||||
|
|
||||||
newWatchers.forEach((email) => {
|
|
||||||
addWatcher({ variables: { jobid, userEmail: email } }).catch((err) =>
|
|
||||||
console.error(`Error adding job watcher: ${err.message}`)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedTeam(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRenderItem = (watcher) => {
|
|
||||||
const employee = bodyshop.employees.find((e) => e.user_email === watcher.user_email);
|
|
||||||
const displayName = employee ? `${employee.first_name} ${employee.last_name}` : watcher.user_email;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
actions={[
|
|
||||||
<Button type="link" danger size="small" onClick={() => handleRemoveWatcher(watcher.user_email)}>
|
|
||||||
{t("notifications.actions.remove")}
|
|
||||||
</Button>
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<List.Item.Meta
|
|
||||||
avatar={<Avatar icon={<UserOutlined />} />}
|
|
||||||
title={<Text>{displayName}</Text>}
|
|
||||||
description={watcher.user_email}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const popoverContent = (
|
|
||||||
<div style={{ width: 600 }}>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
type="text"
|
|
||||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
|
||||||
onClick={handleToggleSelf}
|
|
||||||
loading={adding || removing}
|
|
||||||
>
|
|
||||||
{isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}
|
|
||||||
</Button>
|
|
||||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
|
||||||
{t("notifications.labels.watching-issue")}
|
|
||||||
</Text>
|
|
||||||
{watcherLoading ? <LoadingSpinner /> : <List dataSource={jobWatchers} renderItem={handleRenderItem} />}
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Text type="secondary">{t("notifications.labels.add-watchers")}</Text>
|
|
||||||
<EmployeeSearchSelectComponent
|
|
||||||
style={{ minWidth: "100%" }}
|
|
||||||
options={bodyshop.employees.filter((e) => jobWatchers.every((w) => w.user_email !== e.user_email))}
|
|
||||||
placeholder={t("notifications.labels.employee-search")}
|
|
||||||
value={selectedWatcher}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSelectedWatcher(value);
|
|
||||||
handleWatcherSelect(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{Enhanced_Payroll && bodyshop?.employee_teams?.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
<Text type="secondary">{t("notifications.labels.add-watchers-team")}</Text>
|
|
||||||
<Select
|
|
||||||
showSearch
|
|
||||||
style={{ minWidth: "100%" }}
|
|
||||||
placeholder={t("notifications.labels.teams-search")}
|
|
||||||
value={selectedTeam}
|
|
||||||
onChange={handleTeamSelect}
|
|
||||||
options={bodyshop.employee_teams.map((team) => {
|
|
||||||
const teamMembers = team.employee_team_members
|
|
||||||
.map((member) => {
|
|
||||||
const employee = bodyshop.employees.find((e) => e.id === member.employeeid);
|
|
||||||
return employee ? employee.user_email : null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: JSON.stringify(teamMembers),
|
|
||||||
label: team.name
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
|
|
||||||
<Tooltip title={isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}>
|
|
||||||
<Button
|
|
||||||
shape="circle"
|
|
||||||
type={isWatching ? "primary" : "default"}
|
|
||||||
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
|
||||||
loading={watcherLoading}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(JobWatcherToggle);
|
|
||||||
@@ -56,8 +56,8 @@ import { DateTimeFormat } from "../../utils/DateFormatter";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import JobWatcherToggle from "./job-watcher-toggle.component.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import JobWatcherToggleContainer from "../../components/job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -325,7 +325,7 @@ export function JobsDetailPage({
|
|||||||
|
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
{scenarioNotificationsOn && <JobWatcherToggle job={job} />}
|
{scenarioNotificationsOn && <JobWatcherToggleContainer job={job} />}
|
||||||
{job.ro_number || t("general.labels.na")}
|
{job.ro_number || t("general.labels.na")}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3782,7 +3782,8 @@
|
|||||||
"show-unread-only": "Show Unread",
|
"show-unread-only": "Show Unread",
|
||||||
"mark-all-read": "Mark Read",
|
"mark-all-read": "Mark Read",
|
||||||
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
"notification-popup-title": "Changes for Job #{{ro_number}}",
|
||||||
"ro-number": "RO #{{ro_number}}"
|
"ro-number": "RO #{{ro_number}}",
|
||||||
|
"no-watchers": "No Watchers"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"remove": "remove"
|
"remove": "remove"
|
||||||
|
|||||||
@@ -3782,7 +3782,8 @@
|
|||||||
"show-unread-only": "",
|
"show-unread-only": "",
|
||||||
"mark-all-read": "",
|
"mark-all-read": "",
|
||||||
"notification-popup-title": "",
|
"notification-popup-title": "",
|
||||||
"ro-number": ""
|
"ro-number": "",
|
||||||
|
"no-watchers": ""
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"remove": ""
|
"remove": ""
|
||||||
|
|||||||
@@ -3782,7 +3782,8 @@
|
|||||||
"show-unread-only": "",
|
"show-unread-only": "",
|
||||||
"mark-all-read": "",
|
"mark-all-read": "",
|
||||||
"notification-popup-title": "",
|
"notification-popup-title": "",
|
||||||
"ro-number": ""
|
"ro-number": "",
|
||||||
|
"no-watchers": ""
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"remove": ""
|
"remove": ""
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS "public"."idx_job_watchers_jobid_user_email_unique";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE UNIQUE INDEX "idx_job_watchers_jobid_user_email_unique" on
|
||||||
|
"public"."job_watchers" using btree ("jobid", "user_email");
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
const getJobAssignmentType = (data) => {
|
const getJobAssignmentType = (data) => {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case "employee_pre":
|
case "employee_prep":
|
||||||
return "Prep";
|
return "Prep";
|
||||||
case "employee_body":
|
case "employee_body":
|
||||||
return "Body";
|
return "Body";
|
||||||
|
|||||||
Reference in New Issue
Block a user