feature/IO-3096-GlobalNotifications - Watchers - Second Version
This commit is contained in:
@@ -349,3 +349,13 @@ export const QUERY_STRIPE_ID = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_ACTIVE_EMPLOYEES_IN_SHOP = gql`
|
||||||
|
query GetActiveEmployeesInShop($shopid: uuid!) {
|
||||||
|
associations(where: { shopid: { _eq: $shopid } }) {
|
||||||
|
id
|
||||||
|
useremail
|
||||||
|
shopid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,71 +1,154 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { GET_JOB_WATCHERS, ADD_JOB_WATCHER, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
|
import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
|
||||||
import { Button, Tooltip } from "antd";
|
import { Avatar, Button, List, Popover, Tooltip, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
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"; // Ensure correct path
|
||||||
|
|
||||||
const JobWatcherToggle = ({ job, currentUser }) => {
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const JobWatcherToggle = ({ job, currentUser, bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const userEmail = currentUser.email;
|
const userEmail = currentUser.email;
|
||||||
const jobid = job.id;
|
const jobid = job.id;
|
||||||
|
|
||||||
// Fetch current watchers
|
const [open, setOpen] = useState(false);
|
||||||
const { data, loading } = useQuery(GET_JOB_WATCHERS, {
|
const [selectedWatcher, setSelectedWatcher] = useState(null); // New state for selected value
|
||||||
variables: { jobid }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract current watchers list
|
// Fetch current watchers
|
||||||
const jobWatchers = useMemo(() => data?.job_watchers || [], [data]);
|
const { data: watcherData, loading: watcherLoading } = useQuery(GET_JOB_WATCHERS, { variables: { jobid } });
|
||||||
const isWatching = useMemo(() => !!jobWatchers.find((w) => w.user_email === userEmail), [jobWatchers, userEmail]);
|
|
||||||
|
// Extract watchers list
|
||||||
|
const jobWatchers = useMemo(() => watcherData?.job_watchers || [], [watcherData]);
|
||||||
|
const isWatching = useMemo(() => jobWatchers.some((w) => w.user_email === userEmail), [jobWatchers, userEmail]);
|
||||||
|
|
||||||
// Add watcher mutation
|
// Add watcher mutation
|
||||||
const [addWatcher] = useMutation(ADD_JOB_WATCHER, {
|
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
|
||||||
variables: { jobid, userEmail },
|
|
||||||
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove watcher mutation
|
// Remove watcher mutation
|
||||||
const [removeWatcher] = useMutation(REMOVE_JOB_WATCHER, {
|
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
|
||||||
variables: { jobid, userEmail },
|
|
||||||
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
refetchQueries: [{ query: GET_JOB_WATCHERS, variables: { jobid } }]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle watcher status
|
// Toggle watcher for self
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggleSelf = useCallback(() => {
|
||||||
if (!isWatching) {
|
(isWatching
|
||||||
// Fix: Add if not watching, remove if watching
|
? removeWatcher({ variables: { jobid, userEmail } })
|
||||||
addWatcher().catch((err) => console.error(`Something went wrong adding a job watcher: ${err.message}`));
|
: addWatcher({ variables: { jobid, userEmail } })
|
||||||
} else {
|
).catch((err) => console.error(`Error updating job watcher: ${err.message}`));
|
||||||
removeWatcher().catch((err) => console.error(`Something went wrong removing a job watcher: ${err.message}`));
|
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail]);
|
||||||
}
|
|
||||||
}, [isWatching, addWatcher, removeWatcher]);
|
|
||||||
|
|
||||||
if (loading) {
|
// Handle removing a watcher
|
||||||
return (
|
const handleRemoveWatcher = (userEmail) => {
|
||||||
<Tooltip title={t("notifications.tooltips.unwatch")}>
|
removeWatcher({ variables: { jobid, userEmail } }).catch((err) =>
|
||||||
<Button
|
console.error(`Error removing job watcher: ${err.message}`)
|
||||||
shape="circle"
|
|
||||||
type="primary"
|
|
||||||
icon={<EyeFilled />}
|
|
||||||
disabled
|
|
||||||
aria-label={t("notifications.aria.toggle")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
setSelectedWatcher(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Popover content
|
||||||
|
const popoverContent = (
|
||||||
|
<div style={{ width: 600 }}>
|
||||||
|
{/* Self-toggle Button */}
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="text"
|
||||||
|
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||||
|
onClick={handleToggleSelf}
|
||||||
|
loading={adding || removing}
|
||||||
|
>
|
||||||
|
{isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* List of Watchers */}
|
||||||
|
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||||
|
{t("notifications.labels.watching-issue")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{watcherLoading ? (
|
||||||
|
<LoadingSpinner />
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
dataSource={jobWatchers}
|
||||||
|
renderItem={(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} // Keep the email for reference
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Employee Search Select (for adding watchers) */}
|
||||||
|
<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("production.labels.employeesearch")}
|
||||||
|
value={selectedWatcher} // Controlled value
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedWatcher(value); // Update selected state
|
||||||
|
handleWatcherSelect(value); // Add watcher logic
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}>
|
<Popover content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
|
||||||
<Button
|
<Tooltip title={isWatching ? t("notifications.tooltips.unwatch") : t("notifications.tooltips.watch")}>
|
||||||
shape="circle"
|
<Button
|
||||||
type={isWatching ? "primary" : "default"}
|
shape="circle"
|
||||||
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
type={isWatching ? "primary" : "default"}
|
||||||
onClick={handleToggle}
|
icon={isWatching ? <EyeFilled /> : <EyeOutlined />}
|
||||||
aria-label={t("notifications.aria.toggle")}
|
loading={watcherLoading}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JobWatcherToggle;
|
export default connect(mapStateToProps)(JobWatcherToggle);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Icon, {
|
|||||||
BarsOutlined,
|
BarsOutlined,
|
||||||
CalendarFilled,
|
CalendarFilled,
|
||||||
DollarCircleOutlined,
|
DollarCircleOutlined,
|
||||||
EyeOutlined,
|
|
||||||
FileImageFilled,
|
FileImageFilled,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
PrinterFilled,
|
PrinterFilled,
|
||||||
@@ -326,7 +325,7 @@ export function JobsDetailPage({
|
|||||||
|
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<JobWatcherToggle job={job} currentUser={bodyshop} />
|
<JobWatcherToggle job={job} />
|
||||||
{job.ro_number || t("general.labels.na")}
|
{job.ro_number || t("general.labels.na")}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3762,7 +3762,12 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"notificationscenarios": "Notification Scenarios",
|
"notificationscenarios": "Notification Scenarios",
|
||||||
"save": "Save Scenarios"
|
"save": "Save Scenarios",
|
||||||
|
"watching-issue": "Watching",
|
||||||
|
"add-watchers": "Add Watchers"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"remove": "remove"
|
||||||
},
|
},
|
||||||
"aria": {
|
"aria": {
|
||||||
"toggle": "Toggle Watching Job"
|
"toggle": "Toggle Watching Job"
|
||||||
|
|||||||
@@ -3758,6 +3758,41 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"labels": {
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"save": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"add-watchers": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"watch": "",
|
||||||
|
"unwatch": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"tasks-updated-created": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"alternate-transport-changed": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3758,6 +3758,41 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": ""
|
"unique_vendor_name": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"labels": {
|
||||||
|
"notificationscenarios": "",
|
||||||
|
"save": "",
|
||||||
|
"watching-issue": "",
|
||||||
|
"add-watchers": ""
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"remove": ""
|
||||||
|
},
|
||||||
|
"aria": {
|
||||||
|
"toggle": ""
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"watch": "",
|
||||||
|
"unwatch": ""
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"job-assigned-to-me": "",
|
||||||
|
"bill-posted": "",
|
||||||
|
"critical-parts-status-changed": "",
|
||||||
|
"part-marked-back-ordered": "",
|
||||||
|
"new-note-added": "",
|
||||||
|
"supplement-imported": "",
|
||||||
|
"schedule-dates-changed": "",
|
||||||
|
"tasks-updated-created": "",
|
||||||
|
"new-media-added-reassigned": "",
|
||||||
|
"new-time-ticket-posted": "",
|
||||||
|
"intake-delivery-checklist-completed": "",
|
||||||
|
"job-added-to-production": "",
|
||||||
|
"job-status-change": "",
|
||||||
|
"payment-collected-completed": "",
|
||||||
|
"alternate-transport-changed": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user