Compare commits
14 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b44dd844f | ||
|
|
4a7bb07345 | ||
|
|
01fec9fa79 | ||
|
|
2f88d613c3 | ||
|
|
c9467b3982 | ||
|
|
ca1a456312 | ||
|
|
ca4c48bd5c | ||
|
|
e5fd5c8bcb | ||
|
|
46945a24a7 | ||
|
|
be746500a6 | ||
|
|
71c6d9fa94 | ||
|
|
c010665ea9 | ||
|
|
d6fba12cd9 | ||
|
|
f1847ef650 |
@@ -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 = (
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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] }
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
|
|
||||||
//TODO: Find a way to filter out / blur on demand.
|
//TODO: Find a way to filter out / blur on demand.
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="report-center-modal">
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
||||||
<Form.Item name="defaultSorters" hidden />
|
<Form.Item name="defaultSorters" hidden />
|
||||||
@@ -163,13 +163,14 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
{Object.keys(grouped)
|
{Object.keys(grouped)
|
||||||
//.filter((key) => !groupExcludeKeyFilter.includes(key))
|
//.filter((key) => !groupExcludeKeyFilter.includes(key))
|
||||||
.map((key) => (
|
.map((key) => (
|
||||||
<Col md={8} sm={12} key={key}>
|
<Col xs={24} sm={12} md={Object.keys(grouped).length === 1 ? 24 : 8} key={key}>
|
||||||
<Card.Grid
|
<Card.Grid
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
maxHeight: "33vh",
|
maxHeight: "33vh",
|
||||||
overflowY: "scroll"
|
overflowY: "scroll",
|
||||||
|
minWidth: "200px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography.Title level={4}>{t(`reportcenter.labels.groups.${key}`)}</Typography.Title>
|
<Typography.Title level={4}>{t(`reportcenter.labels.groups.${key}`)}</Typography.Title>
|
||||||
@@ -177,7 +178,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
<BlurWrapperComponent
|
<BlurWrapperComponent
|
||||||
featureName={groupExcludeKeyFilter.find((g) => g.key === key).featureName}
|
featureName={groupExcludeKeyFilter.find((g) => g.key === key).featureName}
|
||||||
>
|
>
|
||||||
<ul style={{ listStyleType: "none", columns: "2 auto" }}>
|
<ul style={{ listStyleType: "none", columns: grouped[key].length > 4 ? "2 auto" : "1", padding: 0, margin: 0 }}>
|
||||||
{grouped[key].map((item) => (
|
{grouped[key].map((item) => (
|
||||||
<li key={item.key}>
|
<li key={item.key}>
|
||||||
<Radio key={item.key} value={item.key}>
|
<Radio key={item.key} value={item.key}>
|
||||||
@@ -188,7 +189,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</BlurWrapperComponent>
|
</BlurWrapperComponent>
|
||||||
) : (
|
) : (
|
||||||
<ul style={{ listStyleType: "none", columns: "2 auto" }}>
|
<ul style={{ listStyleType: "none", columns: grouped[key].length > 4 ? "2 auto" : "1", padding: 0, margin: 0 }}>
|
||||||
{grouped[key].map((item) =>
|
{grouped[key].map((item) =>
|
||||||
item.featureNameRestricted ? (
|
item.featureNameRestricted ? (
|
||||||
<li key={item.key}>
|
<li key={item.key}>
|
||||||
|
|||||||
@@ -11,3 +11,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report center modal fixes for column layout
|
||||||
|
.report-center-modal {
|
||||||
|
.ant-form-item .ant-radio-group {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.ant-card-grid {
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
|
||||||
|
.ant-radio-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
span:not(.ant-radio) {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
|||||||
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
{employeeOptions.length > 0 ? (
|
{employeeOptions.length > 0 ? (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
normalize={(value) => (value || []).filter((id) => typeof id === "string" && id.trim() !== "")}
|
||||||
name="notification_followers"
|
name="notification_followers"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
@@ -42,11 +43,6 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
|||||||
options={employeeOptions}
|
options={employeeOptions}
|
||||||
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
onChange={(value) => {
|
|
||||||
// Filter out null or invalid values before passing to Form
|
|
||||||
const cleanedValue = value?.filter((id) => id != null && typeof id === "string" && id.trim() !== "");
|
|
||||||
return cleanedValue;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1156,7 +1156,11 @@
|
|||||||
enable_manual: false
|
enable_manual: false
|
||||||
update:
|
update:
|
||||||
columns:
|
columns:
|
||||||
|
- imexshopid
|
||||||
|
- timezone
|
||||||
- shopname
|
- shopname
|
||||||
|
- notification_followers
|
||||||
|
- state
|
||||||
- md_order_statuses
|
- md_order_statuses
|
||||||
retry_conf:
|
retry_conf:
|
||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
@@ -3680,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
|
||||||
@@ -3698,6 +3703,7 @@
|
|||||||
- deliverchecklist
|
- deliverchecklist
|
||||||
- depreciation_taxes
|
- depreciation_taxes
|
||||||
- dms_allocation
|
- dms_allocation
|
||||||
|
- dms_id
|
||||||
- driveable
|
- driveable
|
||||||
- employee_body
|
- employee_body
|
||||||
- employee_csr
|
- employee_csr
|
||||||
@@ -3956,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
|
||||||
@@ -3975,6 +3982,7 @@
|
|||||||
- deliverchecklist
|
- deliverchecklist
|
||||||
- depreciation_taxes
|
- depreciation_taxes
|
||||||
- dms_allocation
|
- dms_allocation
|
||||||
|
- dms_id
|
||||||
- driveable
|
- driveable
|
||||||
- employee_body
|
- employee_body
|
||||||
- employee_csr
|
- employee_csr
|
||||||
@@ -4245,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
|
||||||
@@ -4264,6 +4273,7 @@
|
|||||||
- deliverchecklist
|
- deliverchecklist
|
||||||
- depreciation_taxes
|
- depreciation_taxes
|
||||||
- dms_allocation
|
- dms_allocation
|
||||||
|
- dms_id
|
||||||
- driveable
|
- driveable
|
||||||
- employee_body
|
- employee_body
|
||||||
- employee_csr
|
- employee_csr
|
||||||
@@ -4634,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
|
||||||
|
|||||||
@@ -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 "dms_id" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "dms_id" text
|
||||||
|
null;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "created_user_email" text
|
||||||
|
null;
|
||||||
@@ -2926,6 +2926,15 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.GET_BODYSHOP_WATCHERS_BY_ID = `
|
||||||
|
query GET_BODYSHOP_BY_ID($id: uuid!) {
|
||||||
|
bodyshops_by_pk(id: $id) {
|
||||||
|
id
|
||||||
|
notification_followers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports.GET_DOCUMENTS_BY_JOB = `
|
exports.GET_DOCUMENTS_BY_JOB = `
|
||||||
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
||||||
jobs_by_pk(id: $jobId) {
|
jobs_by_pk(id: $jobId) {
|
||||||
@@ -3080,17 +3089,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,8 @@ const generateResetLink = async (email) => {
|
|||||||
*/
|
*/
|
||||||
const ensureExternalIdUnique = async (externalId) => {
|
const ensureExternalIdUnique = async (externalId) => {
|
||||||
const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
|
const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
|
||||||
if (resp.bodyshops.length) {
|
|
||||||
throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` };
|
return !!resp.bodyshops.length;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,10 +224,25 @@ const patchPartsManagementProvisioning = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
const partsManagementProvisioning = async (req, res) => {
|
const partsManagementProvisioning = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
|
||||||
|
// Trim and normalize email early
|
||||||
|
const body = {
|
||||||
|
...req.body,
|
||||||
|
userEmail: req.body.userEmail?.trim().toLowerCase()
|
||||||
|
};
|
||||||
|
|
||||||
|
const trim = (value) => (typeof value === "string" ? value.trim() : value);
|
||||||
|
const trimIfString = (value) =>
|
||||||
|
value !== null && value !== undefined && typeof value === "string" ? value.trim() : value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ensure email is present and trimmed before checking registration
|
||||||
|
if (!body.userEmail) {
|
||||||
|
throw { status: 400, message: "userEmail is required" };
|
||||||
|
}
|
||||||
|
|
||||||
await ensureEmailNotRegistered(body.userEmail);
|
await ensureEmailNotRegistered(body.userEmail);
|
||||||
|
|
||||||
requireFields(body, [
|
requireFields(body, [
|
||||||
"external_shop_id",
|
"external_shop_id",
|
||||||
"shopname",
|
"shopname",
|
||||||
@@ -241,27 +255,69 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
"phone",
|
"phone",
|
||||||
"userEmail"
|
"userEmail"
|
||||||
]);
|
]);
|
||||||
await ensureExternalIdUnique(body.external_shop_id);
|
|
||||||
|
|
||||||
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
|
// Trim all top-level string fields
|
||||||
|
const trimmedBody = {
|
||||||
|
...body,
|
||||||
|
external_shop_id: trim(body.external_shop_id),
|
||||||
|
shopname: trim(body.shopname),
|
||||||
|
address1: trim(body.address1),
|
||||||
|
address2: trimIfString(body.address2),
|
||||||
|
city: trim(body.city),
|
||||||
|
state: trim(body.state),
|
||||||
|
zip_post: trim(body.zip_post),
|
||||||
|
country: trim(body.country),
|
||||||
|
email: trim(body.email),
|
||||||
|
phone: trim(body.phone),
|
||||||
|
timezone: trimIfString(body.timezone),
|
||||||
|
logoUrl: trimIfString(body.logoUrl),
|
||||||
|
userPassword: body.userPassword, // passwords should NOT be trimmed (preserves intentional spaces if any, though rare)
|
||||||
|
vendors: Array.isArray(body.vendors)
|
||||||
|
? body.vendors.map((v) => ({
|
||||||
|
name: trim(v.name),
|
||||||
|
street1: trimIfString(v.street1),
|
||||||
|
street2: trimIfString(v.street2),
|
||||||
|
city: trimIfString(v.city),
|
||||||
|
state: trimIfString(v.state),
|
||||||
|
zip: trimIfString(v.zip),
|
||||||
|
country: trimIfString(v.country),
|
||||||
|
email: trimIfString(v.email),
|
||||||
|
cost_center: trimIfString(v.cost_center),
|
||||||
|
phone: trimIfString(v.phone),
|
||||||
|
dmsid: trimIfString(v.dmsid),
|
||||||
|
discount: v.discount ?? 0,
|
||||||
|
due_date: v.due_date ?? null,
|
||||||
|
favorite: v.favorite ?? [],
|
||||||
|
active: v.active ?? true
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const duplicateCheck = await ensureExternalIdUnique(trimmedBody.external_shop_id);
|
||||||
|
|
||||||
|
if (duplicateCheck) {
|
||||||
|
throw { status: 400, message: `external_shop_id '${trimmedBody.external_shop_id}' is already in use.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("admin-create-shop-user", "debug", trimmedBody.userEmail, null, {
|
||||||
request: req.body,
|
request: req.body,
|
||||||
ioadmin: true
|
ioadmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const shopInput = {
|
const shopInput = {
|
||||||
shopname: body.shopname,
|
shopname: trimmedBody.shopname,
|
||||||
address1: body.address1,
|
address1: trimmedBody.address1,
|
||||||
address2: body.address2 || null,
|
address2: trimmedBody.address2,
|
||||||
city: body.city,
|
city: trimmedBody.city,
|
||||||
state: body.state,
|
state: trimmedBody.state,
|
||||||
zip_post: body.zip_post,
|
zip_post: trimmedBody.zip_post,
|
||||||
country: body.country,
|
country: trimmedBody.country,
|
||||||
email: body.email,
|
email: trimmedBody.email,
|
||||||
external_shop_id: body.external_shop_id,
|
external_shop_id: trimmedBody.external_shop_id,
|
||||||
timezone: body.timezone || DefaultNewShop.timezone,
|
timezone: trimmedBody.timezone || DefaultNewShop.timezone,
|
||||||
phone: body.phone,
|
phone: trimmedBody.phone,
|
||||||
logo_img_path: {
|
logo_img_path: {
|
||||||
src: body.logoUrl,
|
src: trimmedBody.logoUrl || null, // allow empty logo
|
||||||
width: "",
|
width: "",
|
||||||
height: "",
|
height: "",
|
||||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||||
@@ -286,35 +342,37 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||||
vendors: {
|
vendors: {
|
||||||
data: body.vendors.map((v) => ({
|
data: trimmedBody.vendors.map((v) => ({
|
||||||
name: v.name,
|
name: v.name,
|
||||||
street1: v.street1 || null,
|
street1: v.street1,
|
||||||
street2: v.street2 || null,
|
street2: v.street2,
|
||||||
city: v.city || null,
|
city: v.city,
|
||||||
state: v.state || null,
|
state: v.state,
|
||||||
zip: v.zip || null,
|
zip: v.zip,
|
||||||
country: v.country || null,
|
country: v.country,
|
||||||
email: v.email || null,
|
email: v.email,
|
||||||
discount: v.discount ?? 0,
|
discount: v.discount,
|
||||||
due_date: v.due_date ?? null,
|
due_date: v.due_date,
|
||||||
cost_center: v.cost_center || null,
|
cost_center: v.cost_center,
|
||||||
favorite: v.favorite ?? [],
|
favorite: v.favorite,
|
||||||
phone: v.phone || null,
|
phone: v.phone,
|
||||||
active: v.active ?? true,
|
active: v.active,
|
||||||
dmsid: v.dmsid || null
|
dmsid: v.dmsid
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newShopId = await insertBodyshop(shopInput);
|
const newShopId = await insertBodyshop(shopInput);
|
||||||
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
const userRecord = await createFirebaseUser(trimmedBody.userEmail, trimmedBody.userPassword);
|
||||||
let resetLink = null;
|
let resetLink = null;
|
||||||
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
|
if (!trimmedBody.userPassword) {
|
||||||
|
resetLink = await generateResetLink(trimmedBody.userEmail);
|
||||||
|
}
|
||||||
|
|
||||||
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
|
const createdUser = await insertUserAssociation(userRecord.uid, trimmedBody.userEmail, newShopId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
shop: { id: newShopId, shopname: body.shopname },
|
shop: { id: newShopId, shopname: trimmedBody.shopname },
|
||||||
user: {
|
user: {
|
||||||
id: createdUser.id,
|
id: createdUser.id,
|
||||||
email: createdUser.email,
|
email: createdUser.email,
|
||||||
@@ -322,7 +380,7 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
|
logger.log("admin-create-shop-user-error", "error", body.userEmail || "unknown", null, {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: err.detail || err
|
detail: err.detail || err
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
* This module handles automatically adding watchers to new jobs based on the notifications_autoadd
|
* This module handles automatically adding watchers to new jobs based on the notifications_autoadd
|
||||||
* boolean field in the associations table and the notification_followers JSON field in the bodyshops table.
|
* boolean field in the associations table and the notification_followers JSON field in the bodyshops table.
|
||||||
* It ensures users are not added twice and logs the process.
|
* It ensures users are not added twice and logs the process.
|
||||||
|
*
|
||||||
|
* NOTE: Bodyshop notification_followers is fetched directly from the DB (Hasura) to avoid stale Redis cache.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
const { client: gqlClient } = require("../graphql-client/graphql-client");
|
||||||
const { isEmpty } = require("lodash");
|
const { isEmpty } = require("lodash");
|
||||||
const {
|
const {
|
||||||
|
GET_BODYSHOP_WATCHERS_BY_ID,
|
||||||
GET_JOB_WATCHERS_MINIMAL,
|
GET_JOB_WATCHERS_MINIMAL,
|
||||||
GET_NOTIFICATION_WATCHERS,
|
GET_NOTIFICATION_WATCHERS,
|
||||||
INSERT_JOB_WATCHERS
|
INSERT_JOB_WATCHERS
|
||||||
@@ -26,10 +29,7 @@ const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "fa
|
|||||||
*/
|
*/
|
||||||
const autoAddWatchers = async (req) => {
|
const autoAddWatchers = async (req) => {
|
||||||
const { event, trigger } = req.body;
|
const { event, trigger } = req.body;
|
||||||
const {
|
const { logger } = req;
|
||||||
logger,
|
|
||||||
sessionUtils: { getBodyshopFromRedis }
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
// Validate that this is an INSERT event, bail
|
// Validate that this is an INSERT event, bail
|
||||||
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
|
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
|
||||||
@@ -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`);
|
||||||
@@ -48,20 +49,21 @@ const autoAddWatchers = async (req) => {
|
|||||||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch bodyshop data from Redis
|
// Fetch bodyshop data directly from DB (avoid Redis staleness)
|
||||||
const bodyshopData = await getBodyshopFromRedis(shopId);
|
const bodyshopResponse = await gqlClient.request(GET_BODYSHOP_WATCHERS_BY_ID, { id: shopId });
|
||||||
let notificationFollowers = bodyshopData?.notification_followers;
|
const bodyshopData = bodyshopResponse?.bodyshops_by_pk;
|
||||||
|
|
||||||
// Bail if notification_followers is missing or not an array
|
const notificationFollowersRaw = bodyshopData?.notification_followers;
|
||||||
if (!notificationFollowers || !Array.isArray(notificationFollowers)) {
|
const notificationFollowers = Array.isArray(notificationFollowersRaw)
|
||||||
return;
|
? [...new Set(notificationFollowersRaw.filter((id) => id))] // de-dupe + remove falsy
|
||||||
}
|
: [];
|
||||||
|
|
||||||
// Execute queries in parallel
|
// Execute queries in parallel
|
||||||
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.filter((id) => id)
|
employeeIds: notificationFollowers,
|
||||||
|
createdUserEmail
|
||||||
}),
|
}),
|
||||||
gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId })
|
gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId })
|
||||||
]);
|
]);
|
||||||
@@ -73,7 +75,7 @@ const autoAddWatchers = async (req) => {
|
|||||||
associationId: assoc.id
|
associationId: assoc.id
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// Get users from notification_followers
|
// Get users from notification_followers (employee IDs -> employee emails)
|
||||||
const followerEmails =
|
const followerEmails =
|
||||||
notificationData?.employees
|
notificationData?.employees
|
||||||
?.filter((e) => e.user_email)
|
?.filter((e) => e.user_email)
|
||||||
@@ -84,7 +86,7 @@ const autoAddWatchers = async (req) => {
|
|||||||
|
|
||||||
// Combine and deduplicate emails (use email as the unique key)
|
// Combine and deduplicate emails (use email as the unique key)
|
||||||
const usersToAdd = [...autoAddUsers, ...followerEmails].reduce((acc, user) => {
|
const usersToAdd = [...autoAddUsers, ...followerEmails].reduce((acc, user) => {
|
||||||
if (!acc.some((u) => u.email === user.email)) {
|
if (user?.email && !acc.some((u) => u.email === user.email)) {
|
||||||
acc.push(user);
|
acc.push(user);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -123,6 +125,7 @@ const autoAddWatchers = async (req) => {
|
|||||||
message: error?.message,
|
message: error?.message,
|
||||||
stack: error?.stack,
|
stack: error?.stack,
|
||||||
jobId,
|
jobId,
|
||||||
|
shopId,
|
||||||
roNumber
|
roNumber
|
||||||
});
|
});
|
||||||
throw error; // Re-throw to ensure the error is logged in the handler
|
throw error; // Re-throw to ensure the error is logged in the handler
|
||||||
|
|||||||
Reference in New Issue
Block a user