IO-710 Export Log Setup. [ci skip]

This commit is contained in:
Patrick Fic
2021-04-21 12:08:32 -07:00
parent 5f9e813940
commit 6d9576b9a4
26 changed files with 780 additions and 8 deletions

View File

@@ -260,6 +260,11 @@ function Header({
{t("menus.header.accounting-payments")}
</Link>
</Menu.Item>
<Menu.Item key="export-logs">
<Link to="/manage/accounting/exportlogs">
{t("menus.header.export-logs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>

View File

@@ -9,6 +9,7 @@ import { auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -17,6 +18,7 @@ const mapStateToProps = createStructuredSelector({
export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
logImEXEvent("jobs_close_export");
@@ -72,14 +74,43 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
if (failedTransactions.length > 0) {
//Uh oh. At least one was no good.
failedTransactions.map((ft) =>
notification["error"]({
failedTransactions.forEach((ft) => {
//insert failed export log
notification.open({
key: "failedexports",
type: "error",
message: t("jobs.errors.exporting", {
error: ft.errorMessage || "",
}),
})
);
});
//Call is not awaited as it is not critical to finish before proceeding.
insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
success: false,
message: JSON.stringify(ft.errorMessage),
},
],
},
});
});
} else {
//Insert success export log.
insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
success: true,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
@@ -90,8 +121,10 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
},
});
if (!!!jobUpdateResponse.errors) {
notification["success"]({
if (!jobUpdateResponse.errors) {
notification.open({
type: "error",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {

View File

@@ -2,6 +2,7 @@ const ret = {
"accounting:payables": 1,
"accounting:payments": 1,
"accounting:receivables": 1,
"accounting:exportlogs": 3,
"csi:page": 6,
"csi:export": 5,

View File

@@ -21,6 +21,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[

View File

@@ -76,3 +76,42 @@ export const QUERY_PAYMENTS_FOR_EXPORT = gql`
}
}
`;
export const INSERT_EXPORT_LOG = gql`
mutation INSERT_EXPORT_LOG($logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
}
`;
export const QUERY_EXPORT_LOG_PAGINATED = gql`
query QUERY_ALL_EXPORTLOG_PAGINATED(
$offset: Int
$limit: Int
$order: [exportlog_order_by!]
) {
exportlog(offset: $offset, limit: $limit, order_by: $order) {
id
job {
ro_number
id
}
payment {
id
paymentnum
}
bill {
invoice_number
id
}
successful
message
created_at
}
exportlog_aggregate {
aggregate {
count(distinct: true)
}
}
}
`;

View File

@@ -108,6 +108,7 @@ export const QUERY_PARTS_QUEUE = gql`
}
}
`;
export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION {
jobs(where: { inproduction: { _eq: true } }) {

View File

@@ -0,0 +1,230 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import _ from "lodash";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsQueuePageComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, search } = searchParams;
const history = useHistory();
const { loading, error, data, refetch } = useQuery(
QUERY_EXPORT_LOG_PAGINATED,
{
variables: {
//search: search || "",
offset: page ? (page - 1) * 25 : 0,
limit: 25,
order: sortcolumn && [
{
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
}
);
const { t } = useTranslation();
if (error) return <AlertComponent message={error.message} type="error" />;
const handleTableChange = (pagination, filters, sorter) => {
searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
if (filters.status) {
searchParams.statusFilters = JSON.stringify(
_.flattenDeep(filters.status)
);
} else {
delete searchParams.statusFilters;
}
history.push({ search: queryString.stringify(searchParams) });
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>
{(record.job && record.job.ro_number) || t("general.labels.na")}
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
// sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
// sortOrder: sortcolumn === "owner" && sortorder,
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}
</Link>
) : (
<span>{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
record.ownr_co_nm || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: sortcolumn === "plate_no" && sortorder,
render: (text, record) => {
return record.plate_no ? record.plate_no : "";
},
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
},
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
return record.clm_total ? (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : (
t("general.labels.unknown")
);
},
},
{
title: t("jobs.fields.updated_at"),
dataIndex: "updated_at",
key: "updated_at",
render: (text, record) => (
<TimeAgoFormatter>{record.updated_at}</TimeAgoFormatter>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<JobRemoveFromPartsQueue jobId={record.id} refetch={refetch} />
),
},
];
return (
<Card
extra={
<Space wrap>
{searchParams.search && (
<>
<Typography.Title level={4}>
{t("general.labels.searchresults", {
search: searchParams.search,
})}
</Typography.Title>
<Button
onClick={() => {
delete searchParams.search;
history.push({ search: queryString.stringify(searchParams) });
}}
>
{t("general.actions.clear")}
</Button>
</>
)}
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={searchParams.search || t("general.labels.search")}
onSearch={(value) => {
searchParams.search = value;
history.push({ search: queryString.stringify(searchParams) });
}}
/>
</Space>
}
>
<Table
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: data && data.exportlog_aggregate.aggregate.count,
}}
columns={columns}
rowKey="id"
dataSource={data && data.exportlog}
style={{ height: "100%" }}
scroll={{ x: true }}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(PartsQueuePageComponent);

View File

@@ -0,0 +1,36 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import ExportLogsPage from "./export-logs.page.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function ExportsLogPageContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.export-logs");
setSelectedHeader("export-logs");
setBreadcrumbs([
{
link: "/manage/accounting/exportlogs",
label: t("titles.bc.export-logs"),
},
]);
}, [setBreadcrumbs, t, setSelectedHeader]);
return (
<RbacWrapper action="accounting:exportlogs">
<ExportLogsPage />
</RbacWrapper>
);
}
export default connect(null, mapDispatchToProps)(ExportsLogPageContainer);

View File

@@ -150,6 +150,9 @@ const Help = lazy(() => import("../help/help.page"));
const PartsQueue = lazy(() =>
import("../parts-queue/parts-queue.page.container")
);
const ExportLogs = lazy(() =>
import("../export-logs/export-logs.page.container")
);
const EmailTest = lazy(() =>
import("../../components/email-test/email-test-component")
);
@@ -307,7 +310,6 @@ export function Manage({ match, conflict, bodyshop }) {
component={JobsAvailablePage}
/>
<Route exact path={`${match.path}/shop/`} component={ShopPage} />
{
// <Route
// exact
@@ -315,7 +317,6 @@ export function Manage({ match, conflict, bodyshop }) {
// component={ShopTemplates}
// />
}
<Route
exact
path={`${match.path}/shop/vendors`}
@@ -341,7 +342,13 @@ export function Manage({ match, conflict, bodyshop }) {
path={`${match.path}/accounting/payments`}
component={AccountingPayments}
/>
<Route
exact
path={`${match.path}/accounting/exportlogs`}
component={ExportLogs}
/>
<Route exact path={`${match.path}/partsqueue`} component={PartsQueue} />
<Route exact path={`${match.path}/payments`} component={PaymentsAll} />
<Route exact path={`${match.path}/shiftclock`} component={ShiftClock} />
<Route exact path={`${match.path}/scoreboard`} component={Scoreboard} />

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: DROP TABLE "public"."exportlog";
type: run_sql

View File

@@ -0,0 +1,27 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: "CREATE TABLE \"public\".\"exportlog\"(\"id\" uuid NOT NULL DEFAULT gen_random_uuid(),
\"created_at\" timestamptz NOT NULL DEFAULT now(), \"updated_at\" timestamptz
NOT NULL DEFAULT now(), \"jobid\" uuid, \"billid\" uuid, \"paymentid\" uuid,
\"successful\" boolean NOT NULL DEFAULT false, \"message\" text NOT NULL, \"bodyshopid\"
uuid NOT NULL, PRIMARY KEY (\"id\") , FOREIGN KEY (\"jobid\") REFERENCES \"public\".\"jobs\"(\"id\")
ON UPDATE restrict ON DELETE restrict, FOREIGN KEY (\"paymentid\") REFERENCES
\"public\".\"payments\"(\"id\") ON UPDATE restrict ON DELETE restrict, FOREIGN
KEY (\"billid\") REFERENCES \"public\".\"bills\"(\"id\") ON UPDATE restrict
ON DELETE restrict);\nCREATE OR REPLACE FUNCTION \"public\".\"set_current_timestamp_updated_at\"()\nRETURNS
TRIGGER AS $$\nDECLARE\n _new record;\nBEGIN\n _new := NEW;\n _new.\"updated_at\"
= NOW();\n RETURN _new;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER \"set_public_exportlog_updated_at\"\nBEFORE
UPDATE ON \"public\".\"exportlog\"\nFOR EACH ROW\nEXECUTE PROCEDURE \"public\".\"set_current_timestamp_updated_at\"();\nCOMMENT
ON TRIGGER \"set_public_exportlog_updated_at\" ON \"public\".\"exportlog\" \nIS
'trigger to set value of column \"updated_at\" to current timestamp on row update';"
type: run_sql
- args:
name: exportlog
schema: public
type: add_existing_table_or_view

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."exportlog" DROP COLUMN "useremail";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."exportlog" ADD COLUMN "useremail" text NOT NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: alter table "public"."exportlog" drop constraint "exportlog_useremail_fkey";
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: |-
alter table "public"."exportlog"
add constraint "exportlog_useremail_fkey"
foreign key ("useremail")
references "public"."users"
("email") on update restrict on delete restrict;
type: run_sql

View File

@@ -0,0 +1,48 @@
- args:
relationship: exportlogs
table:
name: users
schema: public
type: drop_relationship
- args:
relationship: exportlogs
table:
name: jobs
schema: public
type: drop_relationship
- args:
relationship: exportlogs
table:
name: bills
schema: public
type: drop_relationship
- args:
relationship: exportlogs
table:
name: payments
schema: public
type: drop_relationship
- args:
relationship: payment
table:
name: exportlog
schema: public
type: drop_relationship
- args:
relationship: user
table:
name: exportlog
schema: public
type: drop_relationship
- args:
relationship: job
table:
name: exportlog
schema: public
type: drop_relationship
- args:
relationship: bill
table:
name: exportlog
schema: public
type: drop_relationship

View File

@@ -0,0 +1,80 @@
- args:
name: exportlogs
table:
name: users
schema: public
using:
foreign_key_constraint_on:
column: useremail
table:
name: exportlog
schema: public
type: create_array_relationship
- args:
name: exportlogs
table:
name: jobs
schema: public
using:
foreign_key_constraint_on:
column: jobid
table:
name: exportlog
schema: public
type: create_array_relationship
- args:
name: exportlogs
table:
name: bills
schema: public
using:
foreign_key_constraint_on:
column: billid
table:
name: exportlog
schema: public
type: create_array_relationship
- args:
name: exportlogs
table:
name: payments
schema: public
using:
foreign_key_constraint_on:
column: paymentid
table:
name: exportlog
schema: public
type: create_array_relationship
- args:
name: payment
table:
name: exportlog
schema: public
using:
foreign_key_constraint_on: paymentid
type: create_object_relationship
- args:
name: user
table:
name: exportlog
schema: public
using:
foreign_key_constraint_on: useremail
type: create_object_relationship
- args:
name: job
table:
name: exportlog
schema: public
using:
foreign_key_constraint_on: jobid
type: create_object_relationship
- args:
name: bill
table:
name: exportlog
schema: public
using:
foreign_key_constraint_on: billid
type: create_object_relationship

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: alter table "public"."exportlog" drop constraint "exportlog_bodyshopid_fkey";
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: |-
alter table "public"."exportlog"
add constraint "exportlog_bodyshopid_fkey"
foreign key ("bodyshopid")
references "public"."bodyshops"
("id") on update restrict on delete restrict;
type: run_sql

View File

@@ -0,0 +1,12 @@
- args:
relationship: exportlogs
table:
name: bodyshops
schema: public
type: drop_relationship
- args:
relationship: bodyshop
table:
name: exportlog
schema: public
type: drop_relationship

View File

@@ -0,0 +1,20 @@
- args:
name: exportlogs
table:
name: bodyshops
schema: public
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: exportlog
schema: public
type: create_array_relationship
- args:
name: bodyshop
table:
name: exportlog
schema: public
using:
foreign_key_constraint_on: bodyshopid
type: create_object_relationship

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: exportlog
schema: public
type: drop_insert_permission

View File

@@ -0,0 +1,30 @@
- args:
permission:
allow_upsert: true
backend_only: false
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- jobid
- billid
- paymentid
- successful
- message
- bodyshopid
- useremail
set: {}
role: user
table:
name: exportlog
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: exportlog
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,31 @@
- args:
permission:
allow_aggregations: true
backend_only: false
columns:
- successful
- message
- useremail
- created_at
- updated_at
- billid
- bodyshopid
- id
- jobid
- paymentid
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: exportlog
schema: public
type: create_select_permission

View File

@@ -495,6 +495,13 @@ tables:
table:
schema: public
name: documents
- name: exportlogs
using:
foreign_key_constraint_on:
column: billid
table:
schema: public
name: exportlog
- name: parts_orders
using:
foreign_key_constraint_on:
@@ -691,6 +698,13 @@ tables:
table:
schema: public
name: employees
- name: exportlogs
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: exportlog
- name: jobs
using:
foreign_key_constraint_on:
@@ -1733,6 +1747,73 @@ tables:
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: exportlog
object_relationships:
- name: bill
using:
foreign_key_constraint_on: billid
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
- name: payment
using:
foreign_key_constraint_on: paymentid
- 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:
- id
- created_at
- updated_at
- jobid
- billid
- paymentid
- successful
- message
- bodyshopid
- useremail
backend_only: false
select_permissions:
- role: user
permission:
columns:
- successful
- message
- useremail
- created_at
- updated_at
- billid
- bodyshopid
- id
- jobid
- paymentid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
- table:
schema: public
name: job_conversations
@@ -2167,6 +2248,13 @@ tables:
table:
schema: public
name: documents
- name: exportlogs
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: exportlog
- name: job_conversations
using:
foreign_key_constraint_on:
@@ -3582,6 +3670,14 @@ tables:
- name: job
using:
foreign_key_constraint_on: jobid
array_relationships:
- name: exportlogs
using:
foreign_key_constraint_on:
column: paymentid
table:
schema: public
name: exportlog
insert_permissions:
- role: user
permission:
@@ -3895,6 +3991,13 @@ tables:
table:
schema: public
name: audit_trail
- name: exportlogs
using:
foreign_key_constraint_on:
column: useremail
table:
schema: public
name: exportlog
- name: messages
using:
foreign_key_constraint_on: