Compare commits

..

5 Commits

Author SHA1 Message Date
Allan Carr
eccc992cfa IO-2951 RO Guard Labor Warning
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-16 18:28:54 -08:00
Patrick Fic
d187ed6f73 Merged in release/2025-01-10 (pull request #2049)
Release/2025 01 10
2025-01-10 22:03:04 +00:00
Patrick Fic
68c1ac3e70 Merged in feature/IO-3076-daily-usage-reports (pull request #2047)
Feature/IO-3076 daily usage reports

Approved-by: Dave Richer
2025-01-10 19:47:37 +00:00
Patrick Fic
2f267a9f2c IO-3076 updates to usage report. 2025-01-10 11:39:18 -08:00
Patrick Fic
d6fbf02092 IO-3076 Initial usage reports design. 2025-01-09 11:22:08 -08:00
48 changed files with 322 additions and 545 deletions

1
.gitattributes vendored
View File

@@ -67,7 +67,6 @@
*.rb text eol=lf *.rb text eol=lf
*.java text eol=lf *.java text eol=lf
*.php text eol=lf *.php text eol=lf
*.sql text eol=lf
# Git configuration files # Git configuration files
.gitattributes text eol=lf .gitattributes text eol=lf

View File

@@ -1,6 +1,5 @@
import { EditFilled } from "@ant-design/icons"; import { EditFilled } from "@ant-design/icons";
import { Alert, Card, Col, Row, Space, Table, Typography } from "antd"; import { Alert, Card, Col, Row, Space, Table, Typography } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -9,11 +8,11 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component"; import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
import "./labor-allocations-table.styles.scss"; import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility"; import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician technician: selectTechnician
@@ -65,6 +64,7 @@ export function LaborAllocationsTable({
key: "total", key: "total",
sorter: (a, b) => a.total - b.total, sorter: (a, b) => a.total - b.total,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
align: "right",
render: (text, record) => record.total.toFixed(1) render: (text, record) => record.total.toFixed(1)
}, },
{ {
@@ -73,6 +73,7 @@ export function LaborAllocationsTable({
key: "hrs_claimed", key: "hrs_claimed",
sorter: (a, b) => a.claimed - b.claimed, sorter: (a, b) => a.claimed - b.claimed,
sortOrder: state.sortedInfo.columnKey === "claimed" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "claimed" && state.sortedInfo.order,
align: "right",
render: (text, record) => record.claimed && record.claimed.toFixed(1) render: (text, record) => record.claimed && record.claimed.toFixed(1)
}, },
{ {
@@ -81,6 +82,7 @@ export function LaborAllocationsTable({
key: "adjustments", key: "adjustments",
sorter: (a, b) => a.adjustments - b.adjustments, sorter: (a, b) => a.adjustments - b.adjustments,
sortOrder: state.sortedInfo.columnKey === "adjustments" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "adjustments" && state.sortedInfo.order,
align: "right",
render: (text, record) => ( render: (text, record) => (
<Space wrap> <Space wrap>
{record.adjustments.toFixed(1)} {record.adjustments.toFixed(1)}
@@ -100,17 +102,17 @@ export function LaborAllocationsTable({
{ {
title: t("jobs.labels.difference"), title: t("jobs.labels.difference"),
dataIndex: "difference", dataIndex: "difference",
key: "difference", key: "difference",
sorter: (a, b) => a.difference - b.difference, sorter: (a, b) => a.difference - b.difference,
sortOrder: state.sortedInfo.columnKey === "difference" && state.sortedInfo.order, sortOrder: state.sortedInfo.columnKey === "difference" && state.sortedInfo.order,
align: "right",
render: (text, record) => ( render: (text, record) => (
<strong <strong
style={{ style={{
color: record.difference >= 0 ? "green" : "red" color: record.difference.toFixed(1) >= 0 ? "green" : "red"
}} }}
> >
{_.round(record.difference, 1)} {(Math.abs(record.difference) < 0.05 ? 0 : record.difference).toFixed(1)}
</strong> </strong>
) )
} }
@@ -129,7 +131,6 @@ export function LaborAllocationsTable({
ellipsis: true, ellipsis: true,
render: (text, record) => `${record.op_code_desc || ""}${record.alt_partm ? ` ${record.alt_partm}` : ""}` render: (text, record) => `${record.op_code_desc || ""}${record.alt_partm ? ` ${record.alt_partm}` : ""}`
}, },
{ {
title: t("joblines.fields.act_price"), title: t("joblines.fields.act_price"),
dataIndex: "act_price", dataIndex: "act_price",
@@ -187,7 +188,7 @@ export function LaborAllocationsTable({
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 } { hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
); );
if (summary.difference !== 0 && typeof warningCallback === "function") { if (Math.abs(summary.difference.toFixed(1)) !== 0 && typeof warningCallback === "function") {
warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") }); warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") });
} }
@@ -217,19 +218,21 @@ export function LaborAllocationsTable({
summary={() => ( summary={() => (
<Table.Summary.Row> <Table.Summary.Row>
<Table.Summary.Cell> <Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title> <Typography.Title level={4} style={{ margin: 0, lineHeight: 1 }}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell>{summary.hrs_total.toFixed(1)}</Table.Summary.Cell> <Table.Summary.Cell align="right">{summary.hrs_total.toFixed(1)}</Table.Summary.Cell>
<Table.Summary.Cell>{summary.hrs_claimed.toFixed(1)}</Table.Summary.Cell> <Table.Summary.Cell align="right">{summary.hrs_claimed.toFixed(1)}</Table.Summary.Cell>
<Table.Summary.Cell>{summary.adjustments.toFixed(1)}</Table.Summary.Cell> <Table.Summary.Cell align="right">{summary.adjustments.toFixed(1)}</Table.Summary.Cell>
<Table.Summary.Cell> <Table.Summary.Cell align="right">
<Typography.Text <Typography.Text
style={{ style={{
fontWeight: "bold", fontWeight: "bold",
color: summary.difference >= 0 ? "green" : "red" color: summary.difference.toFixed(1) >= 0 ? "green" : "red"
}} }}
> >
{summary.difference.toFixed(1)} {(Math.abs(summary.difference) < 0.05 ? 0 : summary.difference).toFixed(1)}
</Typography.Text> </Typography.Text>
</Table.Summary.Cell> </Table.Summary.Cell>
</Table.Summary.Row> </Table.Summary.Row>
@@ -261,11 +264,10 @@ export function LaborAllocationsTable({
</Card> </Card>
</Col> </Col>
)} )}
{showWarning && summary.difference !== 0 && ( {showWarning && Math.abs(summary.difference.toFixed(1)) !== 0 && (
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstandinghours")} /> <Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstandinghours")} />
)} )}
</Row> </Row>
); );
} }
export default connect(mapStateToProps, null)(LaborAllocationsTable); export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -207,7 +207,6 @@
- default_prod_list_view - default_prod_list_view
- id - id
- kanban_settings - kanban_settings
- notification_settings
- qbo_realmId - qbo_realmId
- shopid - shopid
- useremail - useremail
@@ -223,7 +222,6 @@
- authlevel - authlevel
- default_prod_list_view - default_prod_list_view
- kanban_settings - kanban_settings
- notification_settings
- qbo_realmId - qbo_realmId
filter: filter:
user: user:
@@ -683,31 +681,6 @@
- exported: - exported:
_eq: false _eq: false
event_triggers: event_triggers:
- name: notifications_bills
definition:
enable_manual: false
insert:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"success": true
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/notifications/events/handleBillsChange'
version: 2
- name: os_bills - name: os_bills
definition: definition:
delete: delete:
@@ -2832,80 +2805,6 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
- table:
name: job_watchers
schema: public
object_relationships:
- name: job
using:
foreign_key_constraint_on: jobid
- name: user
using:
foreign_key_constraint_on: user_email
insert_permissions:
- role: user
permission:
check:
user:
_and:
- associations:
active:
_eq: true
- authid:
_eq: X-Hasura-User-Id
columns:
- user_email
- created_at
- id
- jobid
comment: ""
select_permissions:
- role: user
permission:
columns:
- user_email
- created_at
- id
- jobid
filter:
user:
_and:
- associations:
active:
_eq: true
- authid:
_eq: X-Hasura-User-Id
comment: ""
update_permissions:
- role: user
permission:
columns:
- user_email
- created_at
- id
- jobid
filter:
user:
_and:
- associations:
active:
_eq: true
- authid:
_eq: X-Hasura-User-Id
check: null
comment: ""
delete_permissions:
- role: user
permission:
filter:
user:
_and:
- associations:
active:
_eq: true
- authid:
_eq: X-Hasura-User-Id
comment: ""
- table: - table:
name: joblines name: joblines
schema: public schema: public
@@ -4427,58 +4326,6 @@
template_engine: Kriti template_engine: Kriti
url: '{{$base_url}}/record-handler/arms' url: '{{$base_url}}/record-handler/arms'
version: 2 version: 2
- name: notifications_jobs
definition:
enable_manual: false
insert:
columns: '*'
update:
columns:
- queued_for_parts
- employee_prep
- clm_total
- towin
- employee_body
- converted
- scheduled_in
- scheduled_completion
- scheduled_delivery
- actual_delivery
- actual_completion
- alt_transport
- date_exported
- status
- employee_csr
- actual_in
- deliverchecklist
- comment
- job_totals
- employee_refinish
- inproduction
- production_vars
- intakechecklist
- cieca_ttl
- date_invoiced
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"success": true
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/notifications/events/handleJobsChange'
version: 2
- name: os_jobs - name: os_jobs
definition: definition:
delete: delete:
@@ -4822,57 +4669,6 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
- table:
name: notifications
schema: public
object_relationships:
- name: association
using:
foreign_key_constraint_on: associationid
- name: job
using:
foreign_key_constraint_on: jobid
select_permissions:
- role: user
permission:
columns:
- associationid
- created_at
- fcm_data
- fcm_message
- fcm_title
- id
- jobid
- meta
- read
- ui_translation_meta
- ui_translation_string
- updated_at
filter:
association:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
comment: ""
update_permissions:
- role: user
permission:
columns:
- meta
- read
filter:
association:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: null
comment: ""
- table: - table:
name: owners name: owners
schema: public schema: public
@@ -5113,32 +4909,6 @@
- active: - active:
_eq: true _eq: true
check: null check: null
event_triggers:
- name: notifications_parts_dispatch
definition:
enable_manual: false
insert:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"success": true
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/notifications/events/handlePartsDispatchChange'
version: 2
- table: - table:
name: parts_dispatch_lines name: parts_dispatch_lines
schema: public schema: public
@@ -6109,36 +5879,6 @@
_eq: true _eq: true
check: null check: null
event_triggers: event_triggers:
- name: notifications_tasks
definition:
enable_manual: false
insert:
columns: '*'
update:
columns:
- assigned_to
- completed
- description
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"success": true
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/notifications/events/handleTasksChange'
version: 2
- name: tasks_assigned_changed - name: tasks_assigned_changed
definition: definition:
enable_manual: false enable_manual: false
@@ -6295,32 +6035,6 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
event_triggers:
- name: notifications_time_tickets
definition:
enable_manual: false
insert:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"success": true
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/notifications/events/handleTimeTicketsChange'
version: 2
- table: - table:
name: transitions name: transitions
schema: public schema: public

View File

@@ -39,50 +39,50 @@ END;
$$; $$;
CREATE FUNCTION public.assign_ro_number() RETURNS trigger CREATE FUNCTION public.assign_ro_number() RETURNS trigger
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
begin begin
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
UPDATE counters UPDATE counters
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum' SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
RETURNING concat(prefix,count) into new.ro_number; RETURNING concat(prefix,count) into new.ro_number;
END IF; END IF;
RETURN NEW; RETURN NEW;
END; END;
$$; $$;
CREATE FUNCTION public.audit_trigger() RETURNS trigger CREATE FUNCTION public.audit_trigger() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER LANGUAGE plpgsql SECURITY DEFINER
AS $$ AS $$
DECLARE DECLARE
shopid uuid ; shopid uuid ;
email text; email text;
BEGIN BEGIN
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true; select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
IF TG_OP = 'INSERT' IF TG_OP = 'INSERT'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email); VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
RETURN NEW; RETURN NEW;
ELSIF TG_OP = 'UPDATE' ELSIF TG_OP = 'UPDATE'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email); json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
RETURN NEW; RETURN NEW;
ELSIF TG_OP = 'DELETE' ELSIF TG_OP = 'DELETE'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email); VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
RETURN OLD; RETURN OLD;
END IF; END IF;
END; END;
$$; $$;
CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb
LANGUAGE sql LANGUAGE sql
AS $$ AS $$
SELECT jsonb_object_agg(a.key, a.value) FROM SELECT jsonb_object_agg(a.key, a.value) FROM
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN ( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key ( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
WHERE a.value != b.value OR b.key IS NULL; WHERE a.value != b.value OR b.key IS NULL;
$$; $$;
CREATE TABLE public.bills ( CREATE TABLE public.bills (
id uuid DEFAULT public.gen_random_uuid() NOT NULL, id uuid DEFAULT public.gen_random_uuid() NOT NULL,
@@ -211,33 +211,33 @@ CREATE TABLE public.exportlog (
); );
CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog
LANGUAGE plpgsql STABLE LANGUAGE plpgsql STABLE
AS $$ BEGIN IF search = '' THEN RETURN query AS $$ BEGIN IF search = '' THEN RETURN query
SELECT SELECT
* *
FROM FROM
exportlog e; exportlog e;
ELSE RETURN query ELSE RETURN query
SELECT SELECT
e.* e.*
FROM FROM
exportlog e exportlog e
LEFT JOIN jobs j on j.id = e.jobid LEFT JOIN jobs j on j.id = e.jobid
LEFT JOIN payments p LEFT JOIN payments p
ON p.id = e.paymentid ON p.id = e.paymentid
LEFT JOIN bills b LEFT JOIN bills b
ON e.billid = b.id ON e.billid = b.id
WHERE WHERE
( (
j.ro_number ILIKE '%' || search || '%' j.ro_number ILIKE '%' || search || '%'
OR b.invoice_number ILIKE '%' || search || '%' OR b.invoice_number ILIKE '%' || search || '%'
OR p.paymentnum ILIKE '%' || search || '%' OR p.paymentnum ILIKE '%' || search || '%'
OR e.useremail ILIKE '%' || search || '%' OR e.useremail ILIKE '%' || search || '%'
) )
AND (e.jobid = j.id AND (e.jobid = j.id
or e.paymentid = p.id or e.paymentid = p.id
or e.billid = b.id) or e.billid = b.id)
; ;
END IF; END IF;
END $$; END $$;
CREATE TABLE public.jobs ( CREATE TABLE public.jobs (
id uuid DEFAULT public.gen_random_uuid() NOT NULL, id uuid DEFAULT public.gen_random_uuid() NOT NULL,

View File

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

View File

@@ -1,2 +0,0 @@
alter table "public"."associations" add column "notification_settings" jsonb
null;

View File

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

View File

@@ -1,2 +0,0 @@
CREATE TABLE "public"."notifications" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "job_id" uuid NOT NULL, "association_id" uuid NOT NULL, "ui_translation_string" text NOT NULL, "ui_translation_meta" jsonb, "fcm_title" text NOT NULL, "fcm_message" text NOT NULL, "fcm_data" jsonb, "read" timestamptz, "meta" jsonb, "scenario" Integer NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("job_id") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("association_id") REFERENCES "public"."associations"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"));COMMENT ON TABLE "public"."notifications" IS E'Real Time Notifications System';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -1,3 +0,0 @@
comment on column "public"."notifications"."scenario" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "scenario" drop not null;
alter table "public"."notifications" add column "scenario" int4;

View File

@@ -1 +0,0 @@
alter table "public"."notifications" drop column "scenario" cascade;

View File

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

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" add column "scenario" text
not null;

View File

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

View File

@@ -1,2 +0,0 @@
CREATE TABLE "public"."job_watchers" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "job_id" uuid NOT NULL, "user_email" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("user_email") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("job_id") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"));COMMENT ON TABLE "public"."job_watchers" IS E'Job Watchers';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -1,3 +0,0 @@
comment on column "public"."notifications"."scenario" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "scenario" drop not null;
alter table "public"."notifications" add column "scenario" text;

View File

@@ -1 +0,0 @@
alter table "public"."notifications" drop column "scenario" cascade;

View File

@@ -1,3 +0,0 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- update associations set notification_settings = '{}';

View File

@@ -1 +0,0 @@
update associations set notification_settings = '{}';

View File

@@ -1 +0,0 @@
alter table "public"."associations" alter column "notification_settings" drop not null;

View File

@@ -1 +0,0 @@
alter table "public"."associations" alter column "notification_settings" set not null;

View File

@@ -1,5 +0,0 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- update notifications set meta = '{}';
-- update notifications set fcm_data = '{}';
-- update notifications set ui_translation_meta = '{}';

View File

@@ -1,3 +0,0 @@
update notifications set meta = '{}';
update notifications set fcm_data = '{}';
update notifications set ui_translation_meta = '{}';

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "meta" drop not null;
ALTER TABLE "public"."notifications" ALTER COLUMN "meta" drop default;

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "meta" set default jsonb_build_object();
alter table "public"."notifications" alter column "meta" set not null;

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "fcm_data" drop not null;
ALTER TABLE "public"."notifications" ALTER COLUMN "fcm_data" drop default;

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "fcm_data" set default jsonb_build_object();
alter table "public"."notifications" alter column "fcm_data" set not null;

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "ui_translation_meta" drop not null;
ALTER TABLE "public"."notifications" ALTER COLUMN "ui_translation_meta" drop default;

View File

@@ -1,2 +0,0 @@
alter table "public"."notifications" alter column "ui_translation_meta" set default jsonb_build_object();
alter table "public"."notifications" alter column "ui_translation_meta" set not null;

View File

@@ -1 +0,0 @@
alter table "public"."notifications" rename column "jobid" to "job_id";

View File

@@ -1 +0,0 @@
alter table "public"."notifications" rename column "job_id" to "jobid";

View File

@@ -1 +0,0 @@
alter table "public"."notifications" rename column "associationid" to "association_id";

View File

@@ -1 +0,0 @@
alter table "public"."notifications" rename column "association_id" to "associationid";

View File

@@ -1 +0,0 @@
alter table "public"."job_watchers" rename column "jobid" to "job_id";

View File

@@ -1 +0,0 @@
alter table "public"."job_watchers" rename column "job_id" to "jobid";

View File

@@ -39,50 +39,50 @@ END;
$$; $$;
CREATE FUNCTION public.assign_ro_number() RETURNS trigger CREATE FUNCTION public.assign_ro_number() RETURNS trigger
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
begin begin
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
UPDATE counters UPDATE counters
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum' SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
RETURNING concat(prefix,count) into new.ro_number; RETURNING concat(prefix,count) into new.ro_number;
END IF; END IF;
RETURN NEW; RETURN NEW;
END; END;
$$; $$;
CREATE FUNCTION public.audit_trigger() RETURNS trigger CREATE FUNCTION public.audit_trigger() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER LANGUAGE plpgsql SECURITY DEFINER
AS $$ AS $$
DECLARE DECLARE
shopid uuid ; shopid uuid ;
email text; email text;
BEGIN BEGIN
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true; select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
IF TG_OP = 'INSERT' IF TG_OP = 'INSERT'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email); VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
RETURN NEW; RETURN NEW;
ELSIF TG_OP = 'UPDATE' ELSIF TG_OP = 'UPDATE'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email); json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
RETURN NEW; RETURN NEW;
ELSIF TG_OP = 'DELETE' ELSIF TG_OP = 'DELETE'
THEN THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail) INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email); VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
RETURN OLD; RETURN OLD;
END IF; END IF;
END; END;
$$; $$;
CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb
LANGUAGE sql LANGUAGE sql
AS $$ AS $$
SELECT jsonb_object_agg(a.key, a.value) FROM SELECT jsonb_object_agg(a.key, a.value) FROM
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN ( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key ( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
WHERE a.value != b.value OR b.key IS NULL; WHERE a.value != b.value OR b.key IS NULL;
$$; $$;
CREATE TABLE public.bills ( CREATE TABLE public.bills (
id uuid DEFAULT public.gen_random_uuid() NOT NULL, id uuid DEFAULT public.gen_random_uuid() NOT NULL,
@@ -211,33 +211,33 @@ CREATE TABLE public.exportlog (
); );
CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog
LANGUAGE plpgsql STABLE LANGUAGE plpgsql STABLE
AS $$ BEGIN IF search = '' THEN RETURN query AS $$ BEGIN IF search = '' THEN RETURN query
SELECT SELECT
* *
FROM FROM
exportlog e; exportlog e;
ELSE RETURN query ELSE RETURN query
SELECT SELECT
e.* e.*
FROM FROM
exportlog e exportlog e
LEFT JOIN jobs j on j.id = e.jobid LEFT JOIN jobs j on j.id = e.jobid
LEFT JOIN payments p LEFT JOIN payments p
ON p.id = e.paymentid ON p.id = e.paymentid
LEFT JOIN bills b LEFT JOIN bills b
ON e.billid = b.id ON e.billid = b.id
WHERE WHERE
( (
j.ro_number ILIKE '%' || search || '%' j.ro_number ILIKE '%' || search || '%'
OR b.invoice_number ILIKE '%' || search || '%' OR b.invoice_number ILIKE '%' || search || '%'
OR p.paymentnum ILIKE '%' || search || '%' OR p.paymentnum ILIKE '%' || search || '%'
OR e.useremail ILIKE '%' || search || '%' OR e.useremail ILIKE '%' || search || '%'
) )
AND (e.jobid = j.id AND (e.jobid = j.id
or e.paymentid = p.id or e.paymentid = p.id
or e.billid = b.id) or e.billid = b.id)
; ;
END IF; END IF;
END $$; END $$;
CREATE TABLE public.jobs ( CREATE TABLE public.jobs (
id uuid DEFAULT public.gen_random_uuid() NOT NULL, id uuid DEFAULT public.gen_random_uuid() NOT NULL,

View File

@@ -329,7 +329,8 @@ const main = async () => {
main().catch((error) => { main().catch((error) => {
logger.log(`Main-API-Error: Something was not caught in the application.`, "error", "api", null, { logger.log(`Main-API-Error: Something was not caught in the application.`, "error", "api", null, {
error: error.message, error: error.message,
errorjson: JSON.stringify(error) errorjson: JSON.stringify(error),
stack: error.stack
}); });
// Note: If we want the app to crash on all uncaught async operations, we would // Note: If we want the app to crash on all uncaught async operations, we would
// need to put a `process.exit(1);` here // need to put a `process.exit(1);` here

View File

@@ -3,3 +3,4 @@ exports.autohouse = require("./autohouse").default;
exports.chatter = require("./chatter").default; exports.chatter = require("./chatter").default;
exports.claimscorp = require("./claimscorp").default; exports.claimscorp = require("./claimscorp").default;
exports.kaizen = require("./kaizen").default; exports.kaizen = require("./kaizen").default;
exports.usageReport = require("./usageReport").default;

View File

@@ -0,0 +1,90 @@
const path = require("path");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const client = require("../graphql-client/graphql-client").client;
const emailer = require("../email/sendemail");
const moment = require("moment-timezone");
const converter = require("json-2-csv");
const logger = require("../utils/logger");
const queries = require("../graphql-client/queries");
const InstanceMgr = require("../utils/instanceMgr").default;
exports.default = async (req, res) => {
try {
logger.log("usage-report-email-start", "debug", req?.user?.email, null, {});
if (InstanceMgr({ rome: false, imex: true })) {
//Disable for ImEX at the moment.
res.sendStatus(403);
logger.log("usage-report-email-forbidden", "warn", req?.user?.email, null, {});
return;
}
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
// Validate using autohouse token header.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
logger.log("usage-report-email-forbidden", "warn", req?.user?.email, null, {});
return;
}
//Query the usage data.
const queryResults = await client.request(queries.STATUS_UPDATE, {
today: moment().startOf("day").subtract(3, "days"),
period: moment().subtract(90, "days").startOf("day")
});
//Massage the data.
const shopList = queryResults.bodyshops.map((shop) => ({
"Shop Name": shop.shopname,
"Days Since Creation": moment().diff(moment(shop.created_at), "days"),
"Jobs Created": shop.jobs_created.aggregate.count,
"Jobs Updated": shop.jobs_updated.aggregate.count,
"Owners Created": shop.owners_created.aggregate.count,
"Owners Updated": shop.owners_updated.aggregate.count,
"Vehicles Created": shop.vehicles_created.aggregate.count,
"Vehicles Updated": shop.vehicles_updated.aggregate.count,
"Tasks Created": shop.tasks_created.aggregate.count,
"Tasks Updated": shop.tasks_updated.aggregate.count
}));
const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
emailer
.sendTaskEmail({
to: ["patrick.fic@convenient-brands.com", "bradley.rhoades@convenient-brands.com"],
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
text: `
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers.
Notes:
- Days Since Creation: The number of days since the shop was created. Only shops created in the last 90 days are included.
- Updated values should be higher than created values.
- Counts are inclusive of the last 3 days of data.
`,
attachments: [{ filename: `RO Usage Report ${moment().format("MM/DD/YYYY")}.csv`, content: csv }]
})
.then(() => {
logger.log("usage-report-email-success", "debug", req?.user?.email, null, {
csv
});
})
.catch((error) => {
logger.log("usage-report-email-send-error", "ERROR", req?.user?.email, null, {
error: error.message,
stack: error.stack
});
});
res.sendStatus(200);
return;
} catch (error) {
logger.log("usage-report-email-error", "ERROR", req?.user?.email, null, {
error: error.message,
stack: error.stack
});
res.status(500).json({ error: error.message, stack: error.stack });
}
};

View File

@@ -2617,3 +2617,76 @@ exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conv
} }
} }
`; `;
exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
bodyshops(where: { created_at: { _gte: $period } }) {
shopname
id
created_at
jobs_created: jobs_aggregate(where: { created_at: { _gte: $today } }) {
aggregate {
count
}
}
jobs_updated: jobs_aggregate(where: { updated_at: { _gte: $today } }) {
aggregate {
count
}
}
owners_created: owners_aggregate(where: { created_at: { _gte: $today } }) {
aggregate {
count
}
}
owners_updated: owners_aggregate(where: { updated_at: { _gte: $today } }) {
aggregate {
count
}
}
vehicles_created: vehicles_aggregate(
where: { created_at: { _gte: $today } }
) {
aggregate {
count
}
}
vehicles_updated: vehicles_aggregate(
where: { updated_at: { _gte: $today } }
) {
aggregate {
count
}
}
tasks_created: tasks_aggregate(where: { created_at: { _gte: $today } }) {
aggregate {
count
}
}
tasks_updated: tasks_aggregate(where: { updated_at: { _gte: $today } }) {
aggregate {
count
}
}
jobs {
parts_orders_created: parts_orders_aggregate(
where: { created_at: { _gte: $today } }
) {
aggregate {
count
}
}
parts_orders_updated: parts_orders_aggregate(
where: { updated_at: { _gte: $today } }
) {
aggregate {
count
}
}
}
}
}
`

View File

@@ -1,5 +0,0 @@
const handleJobsChange = (req, res) => {
return res.status(200).json({ message: "Jobs change handled." });
};
module.exports = handleJobsChange;

View File

@@ -1,5 +0,0 @@
const handleBillsChange = (req, res) => {
return res.status(200).json({ message: "Bills change handled." });
};
module.exports = handleBillsChange;

View File

@@ -1,5 +0,0 @@
const handlePartsDispatchChange = (req, res) => {
return res.status(200).json({ message: "Parts Dispatch change handled." });
};
module.exports = handlePartsDispatchChange;

View File

@@ -1,5 +0,0 @@
const handlePartsOrderChange = (req, res) => {
return res.status(200).json({ message: "Parts Order change handled." });
};
module.exports = handlePartsOrderChange;

View File

@@ -1,5 +0,0 @@
const handleTasksChange = (req, res) => {
return res.status(200).json({ message: "Tasks change handled." });
};
module.exports = handleTasksChange;

View File

@@ -1,5 +0,0 @@
const handleTimeTicketsChange = (req, res) => {
return res.status(200).json({ message: "Time Tickets change handled." });
};
module.exports = handleTimeTicketsChange;

View File

@@ -1,10 +1,11 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const { autohouse, claimscorp, chatter, kaizen } = require("../data/data"); const { autohouse, claimscorp, chatter, kaizen, usageReport } = require("../data/data");
router.post("/ah", autohouse); router.post("/ah", autohouse);
router.post("/cc", claimscorp); router.post("/cc", claimscorp);
router.post("/chatter", chatter); router.post("/chatter", chatter);
router.post("/kaizen", kaizen); router.post("/kaizen", kaizen);
router.post("/usagereport", usageReport);
module.exports = router; module.exports = router;

View File

@@ -1,28 +1,12 @@
const express = require("express"); const express = require("express");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { subscribe, unsubscribe, sendNotification } = require("../firebase/firebase-handler"); const { subscribe, unsubscribe, sendNotification } = require("../firebase/firebase-handler");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const handlePartsOrderChange = require("../notifications/eventHandlers/handlePartsOrderChange");
const handlePartsDispatchChange = require("../notifications/eventHandlers/handlePartsDispatchChange");
const handleTasksChange = require("../notifications/eventHandlers/handleTasksChange");
const handleTimeTicketsChange = require("../notifications/eventHandlers/handleTimeTicketsChange");
const handleJobsChange = require("../notifications/eventHandlers/handeJobsChange");
const handleBillsChange = require("../notifications/eventHandlers/handleBillsChange");
const router = express.Router(); const router = express.Router();
// These are FCM handlers router.use(validateFirebaseIdTokenMiddleware);
router.post("/subscribe", validateFirebaseIdTokenMiddleware, subscribe);
router.post("/unsubscribe", validateFirebaseIdTokenMiddleware, unsubscribe);
router.post("/sendtestnotification", validateFirebaseIdTokenMiddleware, sendNotification);
// Hasura Entry points for creating notifications router.post("/subscribe", subscribe);
router.post("/events/handleJobsChange", eventAuthorizationMiddleware, handleJobsChange); router.post("/unsubscribe", unsubscribe);
router.post("/events/handleBillsChange", eventAuthorizationMiddleware, handleBillsChange); router.post("/sendtestnotification", sendNotification);
router.post("/events/handlePartsOrderChange", eventAuthorizationMiddleware, handlePartsOrderChange);
router.post("/events/handlePartsDispatchChange", eventAuthorizationMiddleware, handlePartsDispatchChange);
router.post("/events/handleTasksChange", eventAuthorizationMiddleware, handleTasksChange);
router.post("/events/handleTimeTicketsChange", eventAuthorizationMiddleware, handleTimeTicketsChange);
module.exports = router; module.exports = router;

View File

@@ -41,7 +41,7 @@ exports.taskHandler = async (req, res) => {
return res.status(200).send(csv); return res.status(200).send(csv);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message, stack: error.stackTrace }); res.status(500).json({ error: error.message, stack: error.stack });
} }
}; };