Merged in release/2022-08-26 (pull request #566)

Minor release fixes.

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2022-08-26 21:06:43 +00:00
29 changed files with 878 additions and 37 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -1149,6 +1149,48 @@
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>cc</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>contents</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>created</name>
<definition_loaded>false</definition_loaded>
@@ -1191,6 +1233,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>subject</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>to</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>useremail</name>
<definition_loaded>false</definition_loaded>
@@ -27364,6 +27448,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>emailaudit</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>employeeassignments</name>
<definition_loaded>false</definition_loaded>
@@ -41258,6 +41363,69 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>employeevacation</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>intake</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>manual</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>manualevent</name>
<definition_loaded>false</definition_loaded>

View File

@@ -4,6 +4,8 @@ import { useQuery } from "@apollo/client";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
import { Card, Row } from "antd";
export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
@@ -18,10 +20,20 @@ export default function AuditTrailListContainer({ recordId }) {
{error ? (
<AlertComponent type="error" message={error.message} />
) : (
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : null}
/>
<Row gutter={[16, 16]}>
<Card>
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
<Card>
<EmailAuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : []}
/>
</Card>
</Row>
)}
</div>
);

View File

@@ -0,0 +1,64 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",
key: " created",
width: "10%",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created,
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
},
];
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
{...formItemLayout}
loading={loading}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
}

View File

@@ -77,6 +77,9 @@ export function EmailOverlayContainer({
setSending(true);
try {
await axios.post("/sendemail", {
bodyshopid: bodyshop.id,
jobid: emailConfig.jobid,
...defaultEmailFrom,
ReplyTo: {
Email: from,
@@ -181,10 +184,11 @@ export function EmailOverlayContainer({
loading: sending,
disabled:
selectedMedia &&
( (selectedMedia
(selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
10485760 - new Blob([form.getFieldValue("html")]).size ||
selectedMedia.filter((s) => s.isSelected).length > 10),
}}
>
<Form layout="vertical" form={form} onFinish={handleFinish}>

View File

@@ -1,13 +1,24 @@
import { useQuery } from "@apollo/client";
import { Card, Table } from "antd";
import { Button, Card, Col, Row, Table, Tag } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail);
export default function JobAuditTrail({ jobId }) {
export function JobAuditTrail({ currentUser, jobId }) {
const { t } = useTranslation();
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { jobid: jobId },
skip: !jobId,
fetchPolicy: "network-only",
@@ -34,15 +45,102 @@ export default function JobAuditTrail({ jobId }) {
key: "operation",
},
];
const emailColumns = [
{
title: t("audit.fields.created"),
dataIndex: " created_at",
key: " created_at",
render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
},
{
title: t("audit.fields.to"),
dataIndex: "to",
key: "to",
render: (text, record) =>
record.to &&
record.to.map((email, idx) => <Tag key={idx}>{email}</Tag>),
},
{
title: t("audit.fields.cc"),
dataIndex: "cc",
key: "cc",
render: (text, record) =>
record.cc &&
record.cc.map((email, idx) => <Tag key={idx}>{email}</Tag>),
},
{
title: t("audit.fields.subject"),
dataIndex: "subject",
key: "subject",
},
...(currentUser?.email.includes("@imex.") && [
{
title: t("audit.fields.contents"),
dataIndex: "contents",
key: "contents",
width: "10%",
render: (text, record) => (
<Button
onClick={() => {
var win = window.open(
"",
"Title",
"toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=400,"
);
win.document.body.innerHTML = record.contents;
}}
>
Preview
</Button>
),
},
]),
];
return (
<Card title={t("jobs.labels.audit")}>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
<Row gutter={[16, 16]}>
<Col span={24}>
<Card
title={t("jobs.labels.audit")}
extra={
<Button
onClick={() => {
refetch();
}}
>
<SyncOutlined />
</Button>
}
>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.emailaudit")}>
<Table
loading={loading}
columns={emailColumns}
rowKey="id"
dataSource={data ? data.email_audit_trail : []}
/>
</Card>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,163 @@
import { useMutation } from "@apollo/client";
import {
Button,
Card,
Form,
Input,
Menu,
notification,
Popover,
Select,
Space,
} from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDetailHeaderAddEvent);
export function JobsDetailHeaderAddEvent({ bodyshop, jobid, ...props }) {
const { t } = useTranslation();
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false);
const handleFinish = async (values) => {
logImEXEvent("schedule_manual_event");
setLoading(true);
try {
insertAppointment({
variables: {
apt: { ...values, isintake: false, jobid, bodyshopid: bodyshop.id },
},
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
});
notification.open({
type: "success",
message: t("appointments.successes.created"),
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
setVisibility(false);
}
};
const overlay = (
<Card>
<div>
<Form form={form} layout="vertical" onFinish={handleFinish}>
<Form.Item
label={t("appointments.fields.title")}
name="title"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item label={t("appointments.fields.note")} name="note">
<Input />
</Form.Item>
<Form.Item
label={t("appointments.fields.start")}
name="start"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<FormDateTimePickerComponent
onBlur={() => {
const start = form.getFieldValue("start");
form.setFieldsValue({ end: start.add(30, "minutes") });
}}
/>
</Form.Item>
<Form.Item
label={t("appointments.fields.end")}
name="end"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
if (value) {
const { start } = form.getFieldsValue();
if (moment(start).isAfter(moment(value))) {
return Promise.reject(
t("employees.labels.endmustbeafterstart")
);
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDateTimePickerComponent />
</Form.Item>
<Form.Item label={t("appointments.fields.color")} name="color">
<Select>
{bodyshop.appt_colors.map((col, idx) => (
<Select.Option key={idx} value={col.color.hex}>
{col.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>
<Button type="primary" htmlType="submit" loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
</Card>
);
const handleClick = (e) => {
setVisibility(true);
};
return (
<Popover content={overlay} visible={visibility}>
<Menu.Item {...props} onClick={handleClick}>
{t("appointments.labels.manualevent")}
</Menu.Item>
</Popover>
);
}

View File

@@ -15,6 +15,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -421,6 +422,7 @@ export function JobsDetailHeaderActions({
</Popconfirm>
</Menu.Item>
)}
<JobsDetailHeaderActionsAddevent jobid={job.id} />
{!jobRO && job.converted && (
<Menu.Item>
<Popconfirm

View File

@@ -1,5 +1,5 @@
import { EditFilled } from "@ant-design/icons";
import { Card, Col, Row, Space, Table } from "antd";
import { Card, Col, Row, Space, Table, Typography } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -188,6 +188,19 @@ export function LaborAllocationsTable({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const summary =
totals &&
totals.reduce(
(acc, val) => {
acc.hrs_total += val.total;
acc.hrs_claimed += val.claimed;
acc.adjustments += val.adjustments;
acc.difference += val.difference;
return acc;
},
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
);
return (
<Row gutter={[16, 16]}>
<Col span={24}>
@@ -201,6 +214,27 @@ export function LaborAllocationsTable({
scroll={{
x: true,
}}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.hrs_total.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.hrs_claimed.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.adjustments.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summary.difference.toFixed(1)}
</Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</Card>
</Col>

View File

@@ -39,7 +39,7 @@ export const CalculateAllocationsTotals = (
}, 0),
};
r.difference = (r.total + r.adjustments - r.claimed).toFixed(2);
r.difference = r.total + r.adjustments - r.claimed;
acc.push(r);
return acc;
}, []);

View File

@@ -40,7 +40,10 @@ export function ScheduleCalendarHeaderComponent({
if (!events) return [];
return _.groupBy(
events.filter(
(e) => !e.vacation && moment(date).isSame(moment(e.start), "day")
(e) =>
!e.vacation &&
e.isintake &&
moment(date).isSame(moment(e.start), "day")
),
"job.alt_transport"
);

View File

@@ -1,6 +1,8 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, PageHeader, Row, Space } from "antd";
import React from "react";
import { Button, Card, Checkbox, Col, PageHeader, Row, Space } from "antd";
import { t } from "i18next";
import React, { useMemo } from "react";
import useLocalStorage from "../../utils/useLocalStorage";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
@@ -8,6 +10,23 @@ import ScheduleProductionList from "../schedule-production-list/schedule-product
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
export default function ScheduleCalendarComponent({ data, refetch }) {
const [filter, setFilter] = useLocalStorage("filter_events", {
intake: true,
manual: true,
employeevacation: true,
});
const filteredData = useMemo(() => {
return data.filter(
(d) =>
d.block ||
(filter.intake && d.isintake) ||
(filter.manual && !d.isintake && d.block === false) ||
(d.__typename === "employee_vacation" &&
filter.employeevacation &&
!!d.employee)
);
}, [data, filter]);
return (
<Row gutter={[16, 16]}>
<ScheduleModal />
@@ -16,6 +35,30 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<PageHeader
extra={
<Space wrap>
<Checkbox
checked={filter?.intake}
onChange={(e) => {
setFilter({ ...filter, intake: e.target.checked });
}}
>
{t("schedule.labels.intake")}
</Checkbox>
<Checkbox
checked={filter?.manual}
onChange={(e) => {
setFilter({ ...filter, manual: e.target.checked });
}}
>
{t("schedule.labels.manual")}
</Checkbox>
<Checkbox
checked={filter?.employeevacation}
onChange={(e) => {
setFilter({ ...filter, employeevacation: e.target.checked });
}}
>
{t("schedule.labels.employeevacation")}
</Checkbox>
<ScheduleVerifyIntegrity />
<Button
onClick={() => {
@@ -35,7 +78,7 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
<Col span={24}>
<Card>
<ScheduleCalendarWrapperComponent
data={data}
data={filteredData}
refetch={refetch}
style={{ height: "100rem" }}
/>

View File

@@ -13,6 +13,20 @@ export const QUERY_AUDIT_TRAIL = gql`
created
bodyshopid
}
email_audit_trail(
where: { jobid: { _eq: $jobid } }
order_by: { created_at: desc }
) {
cc
contents
created_at
id
jobid
noteid
subject
to
useremail
}
}
`;

View File

@@ -82,8 +82,12 @@
},
"audit": {
"fields": {
"cc": "CC",
"contents": "Contents",
"created": "Time",
"operation": "Operation",
"subject": "Subject",
"to": "To",
"useremail": "User",
"values": "Values"
}
@@ -1614,6 +1618,7 @@
"documents-images": "Images",
"documents-other": "Other Documents",
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
"emailaudit": "Email Audit Trail",
"employeeassignments": "Employee Assignments",
"estimatelines": "Estimate Lines",
"estimator": "Estimator",
@@ -2443,6 +2448,9 @@
},
"schedule": {
"labels": {
"employeevacation": "Employee Vacations",
"intake": "Intake Events",
"manual": "Manual Events",
"manualevent": "Add Manual Event"
}
},

View File

@@ -82,8 +82,12 @@
},
"audit": {
"fields": {
"cc": "",
"contents": "",
"created": "",
"operation": "",
"subject": "",
"to": "",
"useremail": "",
"values": ""
}
@@ -1614,6 +1618,7 @@
"documents-images": "",
"documents-other": "",
"duplicateconfirm": "",
"emailaudit": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
@@ -2443,6 +2448,9 @@
},
"schedule": {
"labels": {
"employeevacation": "",
"intake": "",
"manual": "",
"manualevent": ""
}
},

View File

@@ -82,8 +82,12 @@
},
"audit": {
"fields": {
"cc": "",
"contents": "",
"created": "",
"operation": "",
"subject": "",
"to": "",
"useremail": "",
"values": ""
}
@@ -1614,6 +1618,7 @@
"documents-images": "",
"documents-other": "",
"duplicateconfirm": "",
"emailaudit": "",
"employeeassignments": "",
"estimatelines": "",
"estimator": "",
@@ -2443,6 +2448,9 @@
},
"schedule": {
"labels": {
"employeevacation": "",
"intake": "",
"manual": "",
"manualevent": ""
}
},

View File

@@ -752,6 +752,13 @@
table:
schema: public
name: documents
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: email_audit_trail
- name: employees
using:
foreign_key_constraint_on:
@@ -1827,6 +1834,93 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: email_audit_trail
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
- name: user
using:
foreign_key_constraint_on: useremail
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
backend_only: false
select_permissions:
- role: user
permission:
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
update_permissions:
- role: user
permission:
columns:
- cc
- to
- contents
- subject
- useremail
- created_at
- updated_at
- bodyshopid
- id
- jobid
- noteid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table:
schema: public
name: employee_vacation
@@ -2726,6 +2820,13 @@
table:
schema: public
name: documents
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: email_audit_trail
- name: exportlogs
using:
foreign_key_constraint_on:
@@ -4976,6 +5077,13 @@
table:
schema: public
name: audit_trail
- name: email_audit_trails
using:
foreign_key_constraint_on:
column: useremail
table:
schema: public
name: email_audit_trail
- name: exportlogs
using:
foreign_key_constraint_on:

View File

@@ -0,0 +1 @@
DROP TABLE "public"."email_audit_trail";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."email_audit_trail" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "jobid" uuid, "noteid" uuid, "to" jsonb NOT NULL DEFAULT jsonb_build_array(), "cc" jsonb NOT NULL DEFAULT jsonb_build_array(), "subject" text, "contents" text, "useremail" text NOT NULL, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_email_audit_trail_updated_at"
BEFORE UPDATE ON "public"."email_audit_trail"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_email_audit_trail_updated_at" ON "public"."email_audit_trail"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
DROP TABLE "public"."email_audit_trail";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."email_audit_trail" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "jobid" uuid, "noteid" uuid, "to" jsonb NOT NULL DEFAULT jsonb_build_array(), "cc" jsonb NOT NULL DEFAULT jsonb_build_array(), "subject" text, "contents" text, "useremail" text NOT NULL, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_email_audit_trail_updated_at"
BEFORE UPDATE ON "public"."email_audit_trail"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_email_audit_trail_updated_at" ON "public"."email_audit_trail"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_bodyshopid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_bodyshopid_fkey"
foreign key ("bodyshopid")
references "public"."bodyshops"
("id") on update cascade on delete cascade;

View File

@@ -0,0 +1 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_jobid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_jobid_fkey"
foreign key ("jobid")
references "public"."jobs"
("id") on update cascade on delete cascade;

View File

@@ -0,0 +1 @@
alter table "public"."email_audit_trail" drop constraint "email_audit_trail_useremail_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."email_audit_trail"
add constraint "email_audit_trail_useremail_fkey"
foreign key ("useremail")
references "public"."users"
("email") on update cascade on delete cascade;

View File

@@ -57,10 +57,10 @@ exports.default = async (req, res) => {
TransmitDateTime: moment().format(momentFormat), // Omitted from ARMS docs
},
EventInfo: {
// AssignmentEvent: {
// CreateDateTime:
// job.asgn_date && moment(job.asgn_date).format(momentFormat),
// },
AssignmentEvent: {
CreateDateTime:
job.asgn_date && moment(job.asgn_date).format(momentFormat),
},
// EstimateEvent: {
// UploadDateTime: moment().format(momentFormat),
// },
@@ -258,9 +258,9 @@ exports.default = async (req, res) => {
VINNum: job.v_vin,
},
},
// License: {
// LicensePlateNum: job.plate_no,
// },
License: {
LicensePlateNum: job.plate_no,
},
VehicleDesc: {
//ProductionDate: "2009-10",
ModelYear:
@@ -845,7 +845,7 @@ exports.default = async (req, res) => {
const entegralResponse =
await entegralSoapClient.RepairOrderFolderAddRqAsync(
jobsToPush,
[jobsToPush[0]],
function (err, result, rawResponse, soapHeader, rawRequest) {
fs.writeFileSync(`./logs/arms-request.xml`, rawRequest);
fs.writeFileSync(`./logs/arms-response.xml`, rawResponse);
@@ -910,10 +910,13 @@ function GetDocumentstatus(job, bodyshop) {
function GetRepairStatusCode(job) {
return "25";
}
function GetProductionStageCode(job, bodyshop) {
if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status))
return "8D";
return "33";
const result = (bodyshop.features?.entegral).find(
(k) => k.status === job.status
);
return result?.code || "33";
}
function isEmpty(obj) {

View File

@@ -9,6 +9,9 @@ const axios = require("axios");
let nodemailer = require("nodemailer");
let aws = require("aws-sdk");
const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
const ses = new aws.SES({
apiVersion: "latest",
@@ -141,7 +144,11 @@ exports.sendEmail = async (req, res) => {
subject: req.body.subject,
// info,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
});
res.json({
success: true, //response: info
});
@@ -154,7 +161,12 @@ exports.sendEmail = async (req, res) => {
subject: req.body.subject,
error: err,
});
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({ success: false, error: err });
}
}
@@ -166,3 +178,27 @@ async function getImage(imageUrl) {
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
}
async function logEmail(req, email) {
try {
await client.request(queries.INSERT_EMAIL_AUDIT, {
email: {
to: email.to,
cc: email.cc,
subject: email.subject,
bodyshopid: req.body.bodyshopid,
useremail: req.user.email,
contents: req.body.html,
jobid: req.body.jobid,
},
});
} catch (error) {
logger.log("email-log-error", "error", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
}
}

View File

@@ -1322,6 +1322,8 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS {
entegral_id
md_responsibility_centers
imexshopid
timezone
features
}
}
`;
@@ -1602,3 +1604,10 @@ exports.INSERT_EXPORT_LOG = `
}
}
`;
exports.INSERT_EMAIL_AUDIT = `mutation INSERT_EMAIL_AUDIT($email: email_audit_trail_insert_input!) {
insert_email_audit_trail_one(object: $email) {
id
}
}
`;