Compare commits
22 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63ce7b5c79 | ||
|
|
79a3b58a86 | ||
|
|
45dd3d8cd6 | ||
|
|
4a7bb07345 | ||
|
|
01fec9fa79 | ||
|
|
2f88d613c3 | ||
|
|
c9467b3982 | ||
|
|
ca1a456312 | ||
|
|
ca4c48bd5c | ||
|
|
e5fd5c8bcb | ||
|
|
46945a24a7 | ||
|
|
be746500a6 | ||
|
|
71c6d9fa94 | ||
|
|
c010665ea9 | ||
|
|
6d94ce7e5c | ||
|
|
d6fba12cd9 | ||
|
|
182a8d59ab | ||
|
|
f1847ef650 | ||
|
|
6ea1c291e6 | ||
|
|
05d5c96491 | ||
|
|
f12e40e4c6 | ||
|
|
bb4e671c83 |
@@ -138,7 +138,7 @@ export function App({
|
||||
);
|
||||
}
|
||||
|
||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||
if (!isPartsEntry && currentEula && !currentUser.eulaIsAccepted) {
|
||||
return <Eula />;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,30 +19,35 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
|
||||
});
|
||||
|
||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) {
|
||||
const { t } = useTranslation();
|
||||
const { socket } = useSocket();
|
||||
const notification = useNotification();
|
||||
|
||||
if (!phone) return <></>;
|
||||
|
||||
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||
if (!bodyshop.messagingservicesid) {
|
||||
return <PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href="# "
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (searchingForConversation) return; // Prevent finding the same thing twice.
|
||||
|
||||
const p = parsePhoneNumber(phone, "CA");
|
||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid, socket });
|
||||
openChatByPhone({ phone_num: p.formatInternational(), jobid, socket });
|
||||
} else {
|
||||
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PhoneNumberFormatter>{phone}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={type}>{phone}</PhoneNumberFormatter>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,6 +133,9 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
|
||||
ownr_ln: contract.job.owner.ownr_ln,
|
||||
ownr_co_nm: contract.job.owner.ownr_co_nm,
|
||||
ownr_ph1: contract.job.owner.ownr_ph1,
|
||||
ownr_ph2: contract.job.owner.ownr_ph2,
|
||||
ownr_ph1_ty: contract.job.owner.ownr_ph1_ty,
|
||||
ownr_ph2_ty: contract.job.owner.ownr_ph2_ty,
|
||||
ownr_ea: contract.job.owner.ownr_ea,
|
||||
v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
|
||||
v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
|
||||
|
||||
@@ -256,9 +256,9 @@ export default function DashboardScheduledDeliveryToday({ data, ...cardProps })
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
@@ -397,6 +397,8 @@ export const DashboardScheduledDeliveryTodayGql = `
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_delivery
|
||||
|
||||
@@ -48,6 +48,8 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
ownr_ln: item.job.ownr_ln,
|
||||
ownr_ph1: item.job.ownr_ph1,
|
||||
ownr_ph2: item.job.ownr_ph2,
|
||||
ownr_ph1_ty: item.job.ownr_ph1_ty,
|
||||
ownr_ph2_ty: item.job.ownr_ph2_ty,
|
||||
production_vars: item.job.production_vars,
|
||||
ro_number: item.job.ro_number,
|
||||
suspended: item.job.suspended,
|
||||
@@ -264,8 +266,8 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
@@ -400,6 +402,8 @@ export const DashboardScheduledInTodayGql = `
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
production_vars
|
||||
ro_number
|
||||
suspended
|
||||
|
||||
@@ -256,9 +256,9 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<ChatOpenButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
@@ -397,6 +397,8 @@ export const DashboardScheduledOutTodayGql = `
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_completion
|
||||
|
||||
@@ -55,7 +55,8 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
||||
const useremail = currentUser.email;
|
||||
|
||||
try {
|
||||
const { ...otherFormValues } = formValues;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { accepted_terms, ...otherFormValues } = formValues;
|
||||
|
||||
// Trim the values of the fields before submitting
|
||||
const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => {
|
||||
|
||||
@@ -73,8 +73,8 @@ export default function GlobalSearchOs() {
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={owner} />
|
||||
</span>
|
||||
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={owner?.ownr_ph1_ty}>{owner.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={owner?.ownr_ph2_ty}>{owner.ownr_ph2}</PhoneNumberFormatter>
|
||||
</Space>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -63,8 +63,8 @@ export default function GlobalSearch() {
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={owner} />
|
||||
</span>
|
||||
<PhoneNumberFormatter>{owner.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter>{owner.ownr_ph2}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={owner.ownr_ph1_ty}>{owner.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={owner.ownr_ph2_ty}>{owner.ownr_ph2}</PhoneNumberFormatter>
|
||||
</Space>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -220,10 +220,10 @@ export function ScheduleEventComponent({
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
|
||||
<ChatOpenButton phone={event?.job?.ownr_ph1} type={event?.job?.ownr_ph1_ty} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
||||
<ChatOpenButton phone={event?.job?.ownr_ph2} type={event?.job?.ownr_ph2_ty} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.loss_of_use")}>
|
||||
{(event.job && event.job.loss_of_use) || ""}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Input } from "antd";
|
||||
import { Form, Input, Select } from "antd";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
@@ -7,6 +7,11 @@ import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-forma
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
export default function JobsCreateOwnerInfoNewComponent() {
|
||||
const PHONE_TYPE_OPTIONS = [
|
||||
{ label: t("owners.labels.home"), value: "Home" },
|
||||
{ label: t("owners.labels.work"), value: "Work" },
|
||||
{ label: t("owners.labels.cell"), value: "Cell" }
|
||||
];
|
||||
const [state] = useContext(JobCreateContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -105,26 +110,56 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
||||
]}
|
||||
name={["owner", "data", "ownr_ea"]}
|
||||
>
|
||||
<FormItemEmail
|
||||
//email={form.getFieldValue("ownr_ea")}
|
||||
disabled={!state.owner.new}
|
||||
/>
|
||||
<FormItemEmail disabled={!state.owner.new} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("owners.fields.ownr_ph1")}
|
||||
name={["owner", "data", "ownr_ph1"]}
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "owner.data.ownr_ph1")]}
|
||||
>
|
||||
<FormItemPhone disabled={!state.owner.new} />
|
||||
|
||||
{/* Phone 1 + Type */}
|
||||
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Form.Item
|
||||
name={["owner", "data", "ownr_ph1"]}
|
||||
noStyle
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "owner.data.ownr_ph1")]}
|
||||
>
|
||||
<FormItemPhone disabled={!state.owner.new} style={{ flex: 1, minWidth: 150 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name={["owner", "data", "ownr_ph1_ty"]} noStyle>
|
||||
<Select
|
||||
disabled={!state.owner.new}
|
||||
allowClear
|
||||
placeholder="Type"
|
||||
options={PHONE_TYPE_OPTIONS}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("owners.fields.ownr_ph2")}
|
||||
name={["owner", "data", "ownr_ph2"]}
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "owner.data.ownr_ph2")]}
|
||||
>
|
||||
<FormItemPhone disabled={!state.owner.new} />
|
||||
|
||||
{/* Phone 2 + Type */}
|
||||
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Form.Item
|
||||
name={["owner", "data", "ownr_ph2"]}
|
||||
noStyle
|
||||
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "owner.data.ownr_ph2")]}
|
||||
>
|
||||
<FormItemPhone disabled={!state.owner.new} style={{ flex: 1, minWidth: 150 }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name={["owner", "data", "ownr_ph2_ty"]} noStyle>
|
||||
<Select
|
||||
disabled={!state.owner.new}
|
||||
allowClear
|
||||
placeholder="Type"
|
||||
options={PHONE_TYPE_OPTIONS}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
|
||||
<Input disabled={!state.owner.new} />
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
||||
title: t("owners.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>,
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph1_ty}>{record.ownr_ph1}</PhoneFormatter>,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ph1, b.ownr_ph1),
|
||||
sortOrder: tableState.sortedInfo.columnKey === "ownr_ph1" && tableState.sortedInfo.order
|
||||
},
|
||||
@@ -69,7 +69,7 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
|
||||
title: t("owners.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>,
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph2_ty}>{record.ownr_ph2}</PhoneFormatter>,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ph2, b.ownr_ph2),
|
||||
sortOrder: tableState.sortedInfo.columnKey === "ownr_ph2" && tableState.sortedInfo.order
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ export function JobsDetailHeaderActions({
|
||||
<FormDateTimePickerComponent
|
||||
onBlur={() => {
|
||||
const start = form.getFieldValue("start");
|
||||
form.setFieldsValue({ end: start.add(30, "minutes") });
|
||||
form.setFieldsValue({ end: start?.add(30, "minutes") });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -251,16 +251,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={job.ownr_ph1_ty}>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
<ChatOpenButton type={job.ownr_ph1_ty} phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={job.ownr_ph2_ty}>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
<ChatOpenButton type={job.ownr_ph2_ty} phone={job.ownr_ph2} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||
|
||||
@@ -64,7 +64,11 @@ export default function JobsFindModalComponent({
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.ownr_ph1 ? <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter> : t("general.labels.unknown");
|
||||
return record.ownr_ph1 ? (
|
||||
<PhoneFormatter type={record.ownr_ph1_ty}>{record.ownr_ph1}</PhoneFormatter>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -74,7 +78,11 @@ export default function JobsFindModalComponent({
|
||||
width: "12%",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.ownr_ph2 ? <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter> : t("general.labels.unknown");
|
||||
return record.ownr_ph2 ? (
|
||||
<PhoneFormatter type={record.ownr_ph2_ty}>{record.ownr_ph2}</PhoneFormatter>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -245,7 +253,11 @@ export default function JobsFindModalComponent({
|
||||
>
|
||||
{t("jobs.labels.override_header")}
|
||||
</Checkbox>
|
||||
<Checkbox checked={partsQueueToggle} onChange={(e) => setPartsQueueToggle(e.target.checked)} id="parts_queue_toggle">
|
||||
<Checkbox
|
||||
checked={partsQueueToggle}
|
||||
onChange={(e) => setPartsQueueToggle(e.target.checked)}
|
||||
id="parts_queue_toggle"
|
||||
>
|
||||
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
|
||||
@@ -72,14 +72,14 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph1} jobid={record.id} />
|
||||
render: (text, record) => <StartChatButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.id} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
render: (text, record) => <StartChatButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.id} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
|
||||
@@ -139,7 +139,7 @@ export function JobsList({ bodyshop }) {
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => <ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
|
||||
render: (text, record) => <ChatOpenButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.id} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
@@ -147,7 +147,7 @@ export function JobsList({ bodyshop }) {
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => <ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
render: (text, record) => <ChatOpenButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.id} />
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -140,7 +140,7 @@ export function JobsReadyList({ bodyshop }) {
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => <ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
|
||||
render: (text, record) => <ChatOpenButton type={record.ownr_ph1_ty} phone={record.ownr_ph1} jobid={record.id} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
@@ -148,7 +148,7 @@ export function JobsReadyList({ bodyshop }) {
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => <ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
render: (text, record) => <ChatOpenButton type={record.ownr_ph2_ty} phone={record.ownr_ph2} jobid={record.id} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Input, Tooltip } from "antd";
|
||||
import { Form, Input, Select, Tooltip } from "antd";
|
||||
import { CloseCircleFilled } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
@@ -10,6 +10,12 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
const { t } = useTranslation();
|
||||
const { getFieldValue } = form;
|
||||
|
||||
const PHONE_TYPE_OPTIONS = [
|
||||
{ label: t("owners.labels.home"), value: "Home" },
|
||||
{ label: t("owners.labels.work"), value: "Work" },
|
||||
{ label: t("owners.labels.cell"), value: "Cell" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormFieldsChanged form={form} />
|
||||
@@ -30,6 +36,7 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("owners.forms.address")}>
|
||||
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
|
||||
<Input />
|
||||
@@ -50,6 +57,7 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||
<Form.Item
|
||||
label={t("owners.fields.ownr_ea")}
|
||||
@@ -63,6 +71,8 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
>
|
||||
<FormItemEmail email={getFieldValue("ownr_ea")} />
|
||||
</Form.Item>
|
||||
|
||||
{/* Phone 1 + Type + Opt-out icon */}
|
||||
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Form.Item
|
||||
@@ -72,6 +82,11 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
>
|
||||
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="ownr_ph1_ty" noStyle>
|
||||
<Select allowClear placeholder="Type" options={PHONE_TYPE_OPTIONS} style={{ width: 110 }} />
|
||||
</Form.Item>
|
||||
|
||||
{isPhone1OptedOut && (
|
||||
<Tooltip title={t("consent.text_body")}>
|
||||
<CloseCircleFilled
|
||||
@@ -88,6 +103,8 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* Phone 2 + Type + Opt-out icon */}
|
||||
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Form.Item
|
||||
@@ -97,6 +114,11 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
>
|
||||
<Input style={{ flex: 1, minWidth: "150px" }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="ownr_ph2_ty" noStyle>
|
||||
<Select allowClear placeholder="Type" options={PHONE_TYPE_OPTIONS} style={{ width: 110 }} />
|
||||
</Form.Item>
|
||||
|
||||
{isPhone2OptedOut && (
|
||||
<Tooltip title={t("consent.text_body")}>
|
||||
<CloseCircleFilled
|
||||
@@ -113,6 +135,7 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
@@ -120,6 +143,7 @@ export default function OwnerDetailFormComponent({ form, isPhone1OptedOut, isPho
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<Form.Item label={t("owners.fields.note")} name="note">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -25,8 +25,10 @@ export default function OwnerDetailUpdateJobsComponent({ owner, selectedJobs, di
|
||||
ownr_ea: owner["ownr_ea"],
|
||||
ownr_fn: owner["ownr_fn"],
|
||||
ownr_ph1: owner["ownr_ph1"],
|
||||
ownr_ph1_ty: owner["ownr_ph1_ty"],
|
||||
ownr_ln: owner["ownr_ln"],
|
||||
ownr_ph2: owner["ownr_ph2"],
|
||||
ownr_ph2_ty: owner["ownr_ph2_ty"],
|
||||
ownr_st: owner["ownr_st"],
|
||||
ownr_title: owner["ownr_title"],
|
||||
ownr_zip: owner["ownr_zip"]
|
||||
|
||||
@@ -47,13 +47,13 @@ export default function OwnerFindModalComponent({
|
||||
title: t("owners.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph1_ty}>{record.ownr_ph1}</PhoneFormatter>
|
||||
},
|
||||
{
|
||||
title: t("owners.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph2_ty}>{record.ownr_ph2}</PhoneFormatter>
|
||||
},
|
||||
{
|
||||
title: t("owners.fields.note"),
|
||||
|
||||
@@ -15,10 +15,10 @@ export default function OwnerTagPopoverComponent({ job }) {
|
||||
<OwnerNameDisplay ownerObject={job} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
<PhoneFormatter>{job.ownr_ph1 || ""}</PhoneFormatter>
|
||||
<PhoneFormatter type={job.ownr_ph1_ty}>{job.ownr_ph1 || ""}</PhoneFormatter>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
<PhoneFormatter>{job.ownr_ph2 || ""}</PhoneFormatter>
|
||||
<PhoneFormatter type={job.ownr_ph2_ty}>{job.ownr_ph2 || ""}</PhoneFormatter>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="3" label={t("owners.fields.address")}>
|
||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||
@@ -36,13 +36,10 @@ export default function OwnerTagPopoverComponent({ job }) {
|
||||
<OwnerNameDisplay ownerObject={job.owner} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
<PhoneFormatter>{job.owner.ownr_ph1 || ""}</PhoneFormatter>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
<PhoneFormatter>{job.owner.ownr_ph2 || ""}</PhoneFormatter>
|
||||
<PhoneFormatter type={job.owner.ownr_ph1_ty}>{job.owner.ownr_ph1 || ""}</PhoneFormatter>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="2" label={t("jobs.fields.ownr_ph2")}>
|
||||
<PhoneFormatter>{job.owner.ownr_ph2 || ""}</PhoneFormatter>
|
||||
<PhoneFormatter type={job.owner.ownr_ph2_ty}>{job.owner.ownr_ph2 || ""}</PhoneFormatter>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="3" label={t("owners.fields.address")}>
|
||||
{`${job.owner.ownr_addr1 || ""} ${job.owner.ownr_addr2 || ""} ${
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
render: (text, record) => {
|
||||
return <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>;
|
||||
return <PhoneFormatter type={record.ownr_ph1_ty}>{record.ownr_ph1}</PhoneFormatter>;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -47,7 +47,7 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
render: (text, record) => {
|
||||
return <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>;
|
||||
return <PhoneFormatter type={record.ownr_ph2_ty}>{record.ownr_ph2}</PhoneFormatter>;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -255,14 +255,14 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
ellipsis: true,
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph1_ty}>{record.ownr_ph1}</PhoneFormatter>
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
render: (text, record) => <PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
||||
render: (text, record) => <PhoneFormatter type={record.ownr_ph2_ty}>{record.ownr_ph2}</PhoneFormatter>
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.specialcoveragepolicy"),
|
||||
|
||||
@@ -144,7 +144,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
<Spin spinning={loading}>
|
||||
{record[type] ? (
|
||||
<div>
|
||||
<span>{`${theEmployee.first_name || ""} ${theEmployee.last_name || ""}`}</span>
|
||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -154,13 +154,25 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
||||
<OwnerNameDisplay ownerObject={theJob} />
|
||||
{!technician ? (
|
||||
<>
|
||||
<StartChatButton phone={data.jobs_by_pk.ownr_ph1} jobid={data.jobs_by_pk.id} />
|
||||
<StartChatButton phone={data.jobs_by_pk.ownr_ph2} jobid={data.jobs_by_pk.id} />
|
||||
<StartChatButton
|
||||
type={data.jobs_by_pk.ownr_ph1_ty}
|
||||
phone={data.jobs_by_pk.ownr_ph1}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
<StartChatButton
|
||||
type={data.jobs_by_pk.ownr_ph2_ty}
|
||||
phone={data.jobs_by_pk.ownr_ph2}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PhoneNumberFormatter>{data.jobs_by_pk.ownr_ph1}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter>{data.jobs_by_pk.ownr_ph2}</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={data.jobs_by_pk.ownr_ph1_ty}>
|
||||
{data.jobs_by_pk.ownr_ph1}
|
||||
</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter type={data.jobs_by_pk.ownr_ph2_ty}>
|
||||
{data.jobs_by_pk.ownr_ph2}
|
||||
</PhoneNumberFormatter>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
|
||||
//TODO: Find a way to filter out / blur on demand.
|
||||
return (
|
||||
<div>
|
||||
<div className="report-center-modal">
|
||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} />
|
||||
<Form.Item name="defaultSorters" hidden />
|
||||
@@ -163,13 +163,14 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
{Object.keys(grouped)
|
||||
//.filter((key) => !groupExcludeKeyFilter.includes(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
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "scroll"
|
||||
overflowY: "scroll",
|
||||
minWidth: "200px"
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4}>{t(`reportcenter.labels.groups.${key}`)}</Typography.Title>
|
||||
@@ -177,7 +178,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
<BlurWrapperComponent
|
||||
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) => (
|
||||
<li key={item.key}>
|
||||
<Radio key={item.key} value={item.key}>
|
||||
@@ -188,7 +189,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
||||
</ul>
|
||||
</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) =>
|
||||
item.featureNameRestricted ? (
|
||||
<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>
|
||||
{employeeOptions.length > 0 ? (
|
||||
<Form.Item
|
||||
normalize={(value) => (value || []).filter((id) => typeof id === "string" && id.trim() !== "")}
|
||||
name="notification_followers"
|
||||
rules={[
|
||||
{
|
||||
@@ -42,11 +43,6 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
||||
options={employeeOptions}
|
||||
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||
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>
|
||||
) : (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getFirestore } from "@firebase/firestore";
|
||||
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||
import { store } from "../redux/store";
|
||||
//import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js'
|
||||
// import posthog from 'posthog-js'
|
||||
|
||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
initializeApp(config);
|
||||
@@ -74,7 +74,6 @@ onMessage(messaging, (payload) => {
|
||||
|
||||
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
try {
|
||||
|
||||
const state = stateProp || store.getState();
|
||||
|
||||
const eventParams = {
|
||||
@@ -99,8 +98,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
//amplitude.track(eventName, eventParams);
|
||||
posthog.capture(eventName, eventParams);
|
||||
|
||||
//posthog.capture(eventName, eventParams);
|
||||
} finally {
|
||||
//If it fails, just keep going.
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_ea
|
||||
clm_total
|
||||
id
|
||||
|
||||
@@ -19,7 +19,9 @@ export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_ea
|
||||
ownerid
|
||||
comment
|
||||
@@ -69,6 +71,8 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_ea
|
||||
ownerid
|
||||
comment
|
||||
@@ -122,6 +126,8 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_ea
|
||||
plate_no
|
||||
plate_st
|
||||
@@ -179,6 +185,8 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
|
||||
clm_total
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
special_coverage_policy
|
||||
owner_owing
|
||||
production_vars
|
||||
@@ -249,6 +257,8 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
|
||||
clm_total
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
special_coverage_policy
|
||||
owner_owing
|
||||
production_vars
|
||||
@@ -615,6 +625,8 @@ export const GET_JOB_BY_PK = gql`
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_zip
|
||||
tax_number
|
||||
@@ -631,6 +643,8 @@ export const GET_JOB_BY_PK = gql`
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_zip
|
||||
parts_tax_rates
|
||||
@@ -714,7 +728,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
v_vin
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
notes(where: { pinned: { _eq: true } }, order_by: { updated_at: desc }) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
@@ -830,6 +844,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
comment
|
||||
ownr_ea
|
||||
ca_gst_registrant
|
||||
@@ -1000,7 +1016,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
key
|
||||
type
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1230,6 +1245,8 @@ export const GET_JOB_INFO_FOR_STRIPE = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_ea
|
||||
}
|
||||
}
|
||||
@@ -1443,8 +1460,10 @@ export const QUERY_JOB_FOR_DUPE = gql`
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph1x
|
||||
ownr_ph1_ty
|
||||
ownr_ph2
|
||||
ownr_ph2x
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
@@ -1690,8 +1709,10 @@ export const QUERY_ALL_JOB_FIELDS = gql`
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph1x
|
||||
ownr_ph1_ty
|
||||
ownr_ph2
|
||||
ownr_ph2x
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
@@ -1829,6 +1850,8 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
plate_no
|
||||
plate_st
|
||||
v_vin
|
||||
@@ -1869,6 +1892,8 @@ export const QUERY_SIMPLIFIED_PARTS_PAGINATED_STATUS_FILTERED = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
plate_no
|
||||
plate_st
|
||||
v_vin
|
||||
@@ -2117,6 +2142,8 @@ export const GET_JOB_FOR_CC_CONTRACT = gql`
|
||||
ownr_zip
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2415,7 +2442,7 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
start
|
||||
status
|
||||
}
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
notes(where: { pinned: { _eq: true } }, order_by: { updated_at: desc }) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
@@ -2503,6 +2530,8 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
owner {
|
||||
id
|
||||
preferred_contact
|
||||
@@ -2600,6 +2629,8 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
clm_total
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
special_coverage_policy
|
||||
owner_owing
|
||||
production_vars
|
||||
|
||||
@@ -8,6 +8,8 @@ export const QUERY_SEARCH_OWNER_BY_IDX = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
ownr_city
|
||||
@@ -57,8 +59,10 @@ export const QUERY_OWNER_BY_ID = gql`
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_ln
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
@@ -112,8 +116,10 @@ export const QUERY_ALL_OWNERS = gql`
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_ln
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
@@ -136,8 +142,10 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_ln
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
@@ -164,8 +172,10 @@ export const QUERY_OWNER_FOR_JOB_CREATION = gql`
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_ln
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_st
|
||||
ownr_title
|
||||
ownr_zip
|
||||
|
||||
@@ -177,10 +177,12 @@ export const QUERY_PARTS_ORDER_OEC = gql`
|
||||
ownr_fax
|
||||
ownr_faxx
|
||||
ownr_ph1
|
||||
ownr_ph1_ty
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_ph1x
|
||||
ownr_ph2
|
||||
ownr_ph2_ty
|
||||
ownr_ph2x
|
||||
ownr_st
|
||||
ownr_title
|
||||
|
||||
@@ -22,6 +22,8 @@ export const GLOBAL_SEARCH_QUERY = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ph1_ty
|
||||
ownr_ph2_ty
|
||||
}
|
||||
search_vehicles(args: { search: $search }, limit: 25) {
|
||||
id
|
||||
|
||||
@@ -2590,7 +2590,10 @@
|
||||
"fromclaim": "Current Claim",
|
||||
"fromowner": "Historical Owner Record",
|
||||
"relatedjobs": "Related Jobs",
|
||||
"updateowner": "Update Owner"
|
||||
"updateowner": "Update Owner",
|
||||
"work": "Work",
|
||||
"home": "Home",
|
||||
"cell": "Cell"
|
||||
},
|
||||
"successes": {
|
||||
"delete": "Owner deleted successfully.",
|
||||
|
||||
@@ -2590,7 +2590,10 @@
|
||||
"fromclaim": "",
|
||||
"fromowner": "",
|
||||
"relatedjobs": "",
|
||||
"updateowner": ""
|
||||
"updateowner": "",
|
||||
"work": "",
|
||||
"home": "",
|
||||
"cell": ""
|
||||
},
|
||||
"successes": {
|
||||
"delete": "",
|
||||
|
||||
@@ -2590,7 +2590,10 @@
|
||||
"fromclaim": "",
|
||||
"fromowner": "",
|
||||
"relatedjobs": "",
|
||||
"updateowner": ""
|
||||
"updateowner": "",
|
||||
"work": "",
|
||||
"home": "",
|
||||
"cell": ""
|
||||
},
|
||||
"successes": {
|
||||
"delete": "",
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
//import NumberFormat from "react-number-format";
|
||||
import { Typography } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
|
||||
export default function PhoneNumberFormatter(props) {
|
||||
const p = parsePhoneNumber(props.children || "", "CA");
|
||||
return p ? <span>{p.formatNational()}</span> : null;
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function PhoneNumberFormatter({ children, type }) {
|
||||
const p = parsePhoneNumber(children || "", "CA");
|
||||
if (!p) return null;
|
||||
|
||||
const phone = p.formatNational();
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Text>{phone}</Text>
|
||||
{type ? (
|
||||
<>
|
||||
{" "}
|
||||
<Text type="secondary">({type})</Text>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ if (!import.meta.env.DEV) {
|
||||
"Module specifier, 'fs' does not start",
|
||||
"Module specifier, 'zlib' does not start with",
|
||||
"Messaging: This browser doesn't support the API's required to use the Firebase SDK.",
|
||||
"Failed to update a ServiceWorker for scope"
|
||||
"Failed to update a ServiceWorker for scope",
|
||||
"Network Error"
|
||||
],
|
||||
integrations: [
|
||||
// See docs for support of different versions of variation of react router
|
||||
|
||||
@@ -24,11 +24,13 @@ const lightningCssTargets = browserslistToTargets(
|
||||
})
|
||||
);
|
||||
|
||||
const currentDatePST = new Date()
|
||||
.toLocaleDateString("en-US", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" })
|
||||
.split("/")
|
||||
.reverse()
|
||||
.join("-");
|
||||
const pstFormatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "America/Los_Angeles",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
});
|
||||
const currentDatePST = pstFormatter.format(new Date());
|
||||
|
||||
const getFormattedTimestamp = () =>
|
||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||
|
||||
@@ -1156,7 +1156,11 @@
|
||||
enable_manual: false
|
||||
update:
|
||||
columns:
|
||||
- imexshopid
|
||||
- timezone
|
||||
- shopname
|
||||
- notification_followers
|
||||
- state
|
||||
- md_order_statuses
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
@@ -3680,6 +3684,7 @@
|
||||
- completed_tasks
|
||||
- converted
|
||||
- created_at
|
||||
- created_user_email
|
||||
- cust_pr
|
||||
- date_estimated
|
||||
- date_exported
|
||||
@@ -3698,6 +3703,7 @@
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_allocation
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
- employee_csr
|
||||
@@ -3793,8 +3799,10 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph1x
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_ph2x
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
@@ -3956,6 +3964,7 @@
|
||||
- completed_tasks
|
||||
- converted
|
||||
- created_at
|
||||
- created_user_email
|
||||
- cust_pr
|
||||
- date_estimated
|
||||
- date_exported
|
||||
@@ -3975,6 +3984,7 @@
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_allocation
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
- employee_csr
|
||||
@@ -4071,8 +4081,10 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph1x
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_ph2x
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
@@ -4245,6 +4257,7 @@
|
||||
- completed_tasks
|
||||
- converted
|
||||
- created_at
|
||||
- created_user_email
|
||||
- cust_pr
|
||||
- date_estimated
|
||||
- date_exported
|
||||
@@ -4264,6 +4277,7 @@
|
||||
- deliverchecklist
|
||||
- depreciation_taxes
|
||||
- dms_allocation
|
||||
- dms_id
|
||||
- driveable
|
||||
- employee_body
|
||||
- employee_csr
|
||||
@@ -4360,8 +4374,10 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph1x
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_ph2x
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
@@ -4634,7 +4650,7 @@
|
||||
request_transform:
|
||||
body:
|
||||
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
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
@@ -5161,7 +5177,9 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
- ownr_zip
|
||||
@@ -5186,7 +5204,9 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
- ownr_zip
|
||||
@@ -5222,7 +5242,9 @@
|
||||
- ownr_fn
|
||||
- ownr_ln
|
||||
- ownr_ph1
|
||||
- ownr_ph1_ty
|
||||
- ownr_ph2
|
||||
- ownr_ph2_ty
|
||||
- ownr_st
|
||||
- ownr_title
|
||||
- ownr_zip
|
||||
|
||||
@@ -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"."owners" add column "ownr_ph1_ty" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."owners" add column "ownr_ph1_ty" 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"."owners" add column "ownr_ph2_ty" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."owners" add column "ownr_ph2_ty" 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 "ownr_ph1_ty" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."jobs" add column "ownr_ph1_ty" 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 "ownr_ph2_ty" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."jobs" add column "ownr_ph2_ty" 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 = `
|
||||
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
||||
jobs_by_pk(id: $jobId) {
|
||||
|
||||
@@ -77,9 +77,8 @@ const generateResetLink = async (email) => {
|
||||
*/
|
||||
const ensureExternalIdUnique = async (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 { 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 {
|
||||
// Ensure email is present and trimmed before checking registration
|
||||
if (!body.userEmail) {
|
||||
throw { status: 400, message: "userEmail is required" };
|
||||
}
|
||||
|
||||
await ensureEmailNotRegistered(body.userEmail);
|
||||
|
||||
requireFields(body, [
|
||||
"external_shop_id",
|
||||
"shopname",
|
||||
@@ -241,27 +255,69 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
"phone",
|
||||
"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,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
const shopInput = {
|
||||
shopname: body.shopname,
|
||||
address1: body.address1,
|
||||
address2: body.address2 || null,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
zip_post: body.zip_post,
|
||||
country: body.country,
|
||||
email: body.email,
|
||||
external_shop_id: body.external_shop_id,
|
||||
timezone: body.timezone || DefaultNewShop.timezone,
|
||||
phone: body.phone,
|
||||
shopname: trimmedBody.shopname,
|
||||
address1: trimmedBody.address1,
|
||||
address2: trimmedBody.address2,
|
||||
city: trimmedBody.city,
|
||||
state: trimmedBody.state,
|
||||
zip_post: trimmedBody.zip_post,
|
||||
country: trimmedBody.country,
|
||||
email: trimmedBody.email,
|
||||
external_shop_id: trimmedBody.external_shop_id,
|
||||
timezone: trimmedBody.timezone || DefaultNewShop.timezone,
|
||||
phone: trimmedBody.phone,
|
||||
logo_img_path: {
|
||||
src: body.logoUrl,
|
||||
src: trimmedBody.logoUrl || null, // allow empty logo
|
||||
width: "",
|
||||
height: "",
|
||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||
@@ -286,35 +342,37 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||
vendors: {
|
||||
data: body.vendors.map((v) => ({
|
||||
data: trimmedBody.vendors.map((v) => ({
|
||||
name: v.name,
|
||||
street1: v.street1 || null,
|
||||
street2: v.street2 || null,
|
||||
city: v.city || null,
|
||||
state: v.state || null,
|
||||
zip: v.zip || null,
|
||||
country: v.country || null,
|
||||
email: v.email || null,
|
||||
discount: v.discount ?? 0,
|
||||
due_date: v.due_date ?? null,
|
||||
cost_center: v.cost_center || null,
|
||||
favorite: v.favorite ?? [],
|
||||
phone: v.phone || null,
|
||||
active: v.active ?? true,
|
||||
dmsid: v.dmsid || null
|
||||
street1: v.street1,
|
||||
street2: v.street2,
|
||||
city: v.city,
|
||||
state: v.state,
|
||||
zip: v.zip,
|
||||
country: v.country,
|
||||
email: v.email,
|
||||
discount: v.discount,
|
||||
due_date: v.due_date,
|
||||
cost_center: v.cost_center,
|
||||
favorite: v.favorite,
|
||||
phone: v.phone,
|
||||
active: v.active,
|
||||
dmsid: v.dmsid
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const newShopId = await insertBodyshop(shopInput);
|
||||
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
||||
const userRecord = await createFirebaseUser(trimmedBody.userEmail, trimmedBody.userPassword);
|
||||
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({
|
||||
shop: { id: newShopId, shopname: body.shopname },
|
||||
shop: { id: newShopId, shopname: trimmedBody.shopname },
|
||||
user: {
|
||||
id: createdUser.id,
|
||||
email: createdUser.email,
|
||||
@@ -322,7 +380,7 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
}
|
||||
});
|
||||
} 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,
|
||||
detail: err.detail || err
|
||||
});
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
* 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.
|
||||
* 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 { isEmpty } = require("lodash");
|
||||
const {
|
||||
GET_BODYSHOP_WATCHERS_BY_ID,
|
||||
GET_JOB_WATCHERS_MINIMAL,
|
||||
GET_NOTIFICATION_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 { event, trigger } = req.body;
|
||||
const {
|
||||
logger,
|
||||
sessionUtils: { getBodyshopFromRedis }
|
||||
} = req;
|
||||
const { logger } = req;
|
||||
|
||||
// Validate that this is an INSERT event, bail
|
||||
if (trigger?.name !== "notifications_jobs_autoadd" || event.op !== "INSERT" || event.data.old) {
|
||||
@@ -48,20 +48,20 @@ const autoAddWatchers = async (req) => {
|
||||
const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];
|
||||
|
||||
try {
|
||||
// Fetch bodyshop data from Redis
|
||||
const bodyshopData = await getBodyshopFromRedis(shopId);
|
||||
let notificationFollowers = bodyshopData?.notification_followers;
|
||||
// Fetch bodyshop data directly from DB (avoid Redis staleness)
|
||||
const bodyshopResponse = await gqlClient.request(GET_BODYSHOP_WATCHERS_BY_ID, { id: shopId });
|
||||
const bodyshopData = bodyshopResponse?.bodyshops_by_pk;
|
||||
|
||||
// Bail if notification_followers is missing or not an array
|
||||
if (!notificationFollowers || !Array.isArray(notificationFollowers)) {
|
||||
return;
|
||||
}
|
||||
const notificationFollowersRaw = bodyshopData?.notification_followers;
|
||||
const notificationFollowers = Array.isArray(notificationFollowersRaw)
|
||||
? [...new Set(notificationFollowersRaw.filter((id) => id))] // de-dupe + remove falsy
|
||||
: [];
|
||||
|
||||
// Execute queries in parallel
|
||||
const [notificationData, existingWatchersData] = await Promise.all([
|
||||
gqlClient.request(GET_NOTIFICATION_WATCHERS, {
|
||||
shopId,
|
||||
employeeIds: notificationFollowers.filter((id) => id)
|
||||
employeeIds: notificationFollowers
|
||||
}),
|
||||
gqlClient.request(GET_JOB_WATCHERS_MINIMAL, { jobid: jobId })
|
||||
]);
|
||||
@@ -73,7 +73,7 @@ const autoAddWatchers = async (req) => {
|
||||
associationId: assoc.id
|
||||
})) || [];
|
||||
|
||||
// Get users from notification_followers
|
||||
// Get users from notification_followers (employee IDs -> employee emails)
|
||||
const followerEmails =
|
||||
notificationData?.employees
|
||||
?.filter((e) => e.user_email)
|
||||
@@ -84,7 +84,7 @@ const autoAddWatchers = async (req) => {
|
||||
|
||||
// Combine and deduplicate emails (use email as the unique key)
|
||||
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);
|
||||
}
|
||||
return acc;
|
||||
@@ -123,6 +123,7 @@ const autoAddWatchers = async (req) => {
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
jobId,
|
||||
shopId,
|
||||
roNumber
|
||||
});
|
||||
throw error; // Re-throw to ensure the error is logged in the handler
|
||||
|
||||
Reference in New Issue
Block a user