Merged in release/2025-12-19 (pull request #2741)

feature/IO-3487-Auto-Add-Profile-Watchers - Fix Auto Add on a profile level
This commit is contained in:
Dave Richer
2025-12-22 19:20:34 +00:00
11 changed files with 81 additions and 28 deletions

View File

@@ -253,6 +253,10 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
} }
}; };
if (currentUser?.email) {
newJob.created_user_email = currentUser.email;
}
//Calcualte the new job totals. //Calcualte the new job totals.
const newTotals = ( const newTotals = (

View File

@@ -43,16 +43,18 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines, tec
const handleCreateIou = async () => { const handleCreateIou = async () => {
setLoading(true); setLoading(true);
//Query all of the job details to recreate. //Query all of the job details to recreate.
const iouId = await CreateIouForJob( const iouId = await CreateIouForJob({
client, apolloClient: client,
job.id, jobLinesToKeep: selectedJobLines,
{ jobId: job.id,
config: {
status: bodyshop.md_ro_statuses.default_open, status: bodyshop.md_ro_statuses.default_open,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
useremail: currentUser.email useremail: currentUser.email
}, },
selectedJobLines currentUser
); });
notification.open({ notification.open({
type: "success", type: "success",
message: t("jobs.successes.ioucreated"), message: t("jobs.successes.ioucreated"),

View File

@@ -154,6 +154,10 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
: {}) : {})
}; };
if (currentUser?.email) {
newJob.created_user_email = currentUser.email;
}
if (selectedOwner) { if (selectedOwner) {
newJob.ownerid = selectedOwner; newJob.ownerid = selectedOwner;
delete newJob.owner; delete newJob.owner;

View File

@@ -175,25 +175,33 @@ export function JobsDetailHeaderActions({
}; };
const handleDuplicate = () => const handleDuplicate = () =>
DuplicateJob( DuplicateJob({
client, apolloClient: client,
job.id, jobId: job.id,
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
(newJobId) => { completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification.success({ notification.success({
message: t("jobs.successes.duplicated") message: t("jobs.successes.duplicated")
}); });
}, },
true keepJobLines: true,
); currentUser
});
const handleDuplicateConfirm = () => const handleDuplicateConfirm = () =>
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => { DuplicateJob({
history(`/manage/jobs/${newJobId}`); apolloClient: client,
notification.success({ jobId: job.id,
message: t("jobs.successes.duplicated") config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
}); completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`);
notification.success({
message: t("jobs.successes.duplicated")
});
},
keepJobLines: false,
currentUser
}); });
const handleFinish = async (values) => { const handleFinish = async (values) => {

View File

@@ -5,7 +5,14 @@ import { INSERT_NEW_JOB, QUERY_JOB_FOR_DUPE } from "../../graphql/jobs.queries";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import i18n from "i18next"; import i18n from "i18next";
export default async function DuplicateJob(apolloClient, jobId, config, completionCallback, keepJobLines = false) { export default async function DuplicateJob({
apolloClient,
jobId,
config,
completionCallback,
keepJobLines = false,
currentUser
}) {
logImEXEvent("job_duplicate"); logImEXEvent("job_duplicate");
const { defaultOpenStatus } = config; const { defaultOpenStatus } = config;
@@ -19,6 +26,7 @@ export default async function DuplicateJob(apolloClient, jobId, config, completi
const existingJob = _.cloneDeep(jobs_by_pk); const existingJob = _.cloneDeep(jobs_by_pk);
delete existingJob.__typename; delete existingJob.__typename;
delete existingJob.id; delete existingJob.id;
delete existingJob.created_user_email;
delete existingJob.createdat; delete existingJob.createdat;
delete existingJob.updatedat; delete existingJob.updatedat;
delete existingJob.cieca_stl; delete existingJob.cieca_stl;
@@ -29,6 +37,10 @@ export default async function DuplicateJob(apolloClient, jobId, config, completi
status: defaultOpenStatus status: defaultOpenStatus
}; };
if (currentUser?.email) {
newJob.created_user_email = currentUser.email;
}
const _tempLines = _.cloneDeep(existingJob.joblines); const _tempLines = _.cloneDeep(existingJob.joblines);
_tempLines.forEach((line) => { _tempLines.forEach((line) => {
delete line.id; delete line.id;
@@ -55,7 +67,7 @@ export default async function DuplicateJob(apolloClient, jobId, config, completi
return; return;
} }
export async function CreateIouForJob(apolloClient, jobId, config, jobLinesToKeep) { export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) {
logImEXEvent("job_create_iou"); logImEXEvent("job_create_iou");
const { status } = config; const { status } = config;
@@ -109,6 +121,9 @@ export async function CreateIouForJob(apolloClient, jobId, config, jobLinesToKee
delete newJob.joblines; delete newJob.joblines;
newJob.joblines = { data: _tempLines }; newJob.joblines = { data: _tempLines };
if (currentUser?.email) {
newJob.created_user_email = currentUser.email;
}
const res2 = await apolloClient.mutate({ const res2 = await apolloClient.mutate({
mutation: INSERT_NEW_JOB, mutation: INSERT_NEW_JOB,
variables: { job: [newJob] } variables: { job: [newJob] }

View File

@@ -9,21 +9,23 @@ import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries"; import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries"; import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobsCreateComponent from "./jobs-create.component"; import JobsCreateComponent from "./jobs-create.component";
import JobCreateContext from "./jobs-create.context"; import JobCreateContext from "./jobs-create.context";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop,
currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
}); });
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) {
const { t } = useTranslation(); const { t } = useTranslation();
const notification = useNotification(); const notification = useNotification();
@@ -74,7 +76,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]); }, [t, setBreadcrumbs, setSelectedHeader]);
const runInsertJob = (job) => { const runInsertJob = (job) => {
insertJob({ variables: { job: job } }) insertJob({ variables: { job } })
.then((resp) => { .then((resp) => {
setState({ setState({
...state, ...state,
@@ -150,6 +152,11 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
if (job.owner === null) delete job.owner; if (job.owner === null) delete job.owner;
if (job.vehicle === null) delete job.vehicle; if (job.vehicle === null) delete job.vehicle;
// Associate to the current user if one exists
if (currentUser?.email) {
job.created_user_email = currentUser.email;
}
runInsertJob(job); runInsertJob(job);
}; };

View File

@@ -3684,6 +3684,7 @@
- completed_tasks - completed_tasks
- converted - converted
- created_at - created_at
- created_user_email
- cust_pr - cust_pr
- date_estimated - date_estimated
- date_exported - date_exported
@@ -3961,6 +3962,7 @@
- completed_tasks - completed_tasks
- converted - converted
- created_at - created_at
- created_user_email
- cust_pr - cust_pr
- date_estimated - date_estimated
- date_exported - date_exported
@@ -4251,6 +4253,7 @@
- completed_tasks - completed_tasks
- converted - converted
- created_at - created_at
- created_user_email
- cust_pr - cust_pr
- date_estimated - date_estimated
- date_exported - date_exported
@@ -4641,7 +4644,7 @@
request_transform: request_transform:
body: body:
action: transform action: transform
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n" template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}},\r\n \"x-hasura-role\": {{$body?.event?.session_variables?.x-hasura-role ?? \"Internal\"}}\r\n }, \r\n \"op\": {{$body.event.op}},\r\n \"data\": {\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"shopid\": {{$body.event.data.new?.shopid}},\r\n \"ro_number\": {{$body.event.data.new?.ro_number}},\r\n \"created_user_email\": {{$body.event.data.new?.created_user_email}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs_autoadd\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
method: POST method: POST
query_params: {} query_params: {}
template_engine: Kriti template_engine: Kriti

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "created_user_email" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "created_user_email" text
null;

View File

@@ -3082,17 +3082,19 @@ exports.INSERT_JOB_WATCHERS = `
`; `;
exports.GET_NOTIFICATION_WATCHERS = ` exports.GET_NOTIFICATION_WATCHERS = `
query GET_NOTIFICATION_WATCHERS($shopId: uuid!, $employeeIds: [uuid!]!) { query GET_NOTIFICATION_WATCHERS($shopId: uuid!, $employeeIds: [uuid!]!, $createdUserEmail: String!) {
associations(where: { associations(where: {
_and: [ _and: [
{ shopid: { _eq: $shopId } }, { shopid: { _eq: $shopId } },
{ active: { _eq: true } }, { active: { _eq: true } },
{ notifications_autoadd: { _eq: true } } { notifications_autoadd: { _eq: true } },
{ useremail: { _eq: $createdUserEmail } }
] ]
}) { }) {
id id
useremail useremail
} }
employees(where: { id: { _in: $employeeIds }, shopid: { _eq: $shopId }, active: { _eq: true } }) { employees(where: { id: { _in: $employeeIds }, shopid: { _eq: $shopId }, active: { _eq: true } }) {
user_email user_email
} }

View File

@@ -39,6 +39,7 @@ const autoAddWatchers = async (req) => {
const jobId = event?.data?.new?.id; const jobId = event?.data?.new?.id;
const shopId = event?.data?.new?.shopid; const shopId = event?.data?.new?.shopid;
const roNumber = event?.data?.new?.ro_number || "unknown"; const roNumber = event?.data?.new?.ro_number || "unknown";
const createdUserEmail = event?.data?.new?.created_user_email || "Unknown";
if (!jobId || !shopId) { if (!jobId || !shopId) {
throw new Error(`Missing jobId (${jobId}) or shopId (${shopId}) for auto-add watchers`); throw new Error(`Missing jobId (${jobId}) or shopId (${shopId}) for auto-add watchers`);
@@ -61,7 +62,8 @@ const autoAddWatchers = async (req) => {
const [notificationData, existingWatchersData] = await Promise.all([ const [notificationData, existingWatchersData] = await Promise.all([
gqlClient.request(GET_NOTIFICATION_WATCHERS, { gqlClient.request(GET_NOTIFICATION_WATCHERS, {
shopId, shopId,
employeeIds: notificationFollowers employeeIds: notificationFollowers,
createdUserEmail
}), }),
gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId }) gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId })
]); ]);