Compare commits

..

5 Commits

Author SHA1 Message Date
Dave Richer
2c8f5c7184 feature/IO-3060-Realtime-Notification-System: Weird sql Lind endings 2025-01-15 11:11:03 -08:00
Dave Richer
a06d3c9365 feature/IO-3060-Realtime-Notifications- Checkpoint 2025-01-14 09:00:04 -08:00
Dave Richer
8a0916a47f feature/IO-3060-Realtime-Notifications- Checkpoint 2025-01-13 07:27:10 -08:00
Dave Richer
3bc6504ae6 feature/IO-3060-Realtime-Notifications-Progress Update 2025-01-10 09:33:47 -08:00
Dave Richer
e2e5f3f885 feature/IO-3060-Realtime-Notifications-System
- Add handlers for Job and Bill Change / Register Routers
- Add Tables / Modify Tables / Add permissions.
2025-01-09 13:28:56 -08:00
47 changed files with 554 additions and 163 deletions

1
.gitattributes vendored
View File

@@ -67,6 +67,7 @@
*.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

@@ -173,7 +173,7 @@ export function JobsCloseExportButton({
} }
}); });
if (!jobUpdateResponse.errors) { if (!!!jobUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
@@ -222,7 +222,7 @@ export function JobsCloseExportButton({
}; };
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.export")} {t("jobs.actions.export")}
</Button> </Button>
); );

View File

@@ -10,8 +10,8 @@ import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -165,7 +165,7 @@ export function JobsExportAllButton({
} }
}); });
if (!jobUpdateResponse.errors) { if (!!!jobUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
@@ -213,13 +213,13 @@ export function JobsExportAllButton({
}) })
); );
if (completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
{t("jobs.actions.exportselected")} {t("jobs.actions.exportselected")}
</Button> </Button>
); );

View File

@@ -48,7 +48,7 @@ export function PayableExportAll({
let PartnerResponse; let PartnerResponse;
setLoading(true); setLoading(true);
if (loadingCallback) loadingCallback(true); if (!!loadingCallback) loadingCallback(true);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, { PartnerResponse = await axios.post(`/qbo/payables`, {
bills: billids, bills: billids,
@@ -85,7 +85,7 @@ export function PayableExportAll({
notification["error"]({ notification["error"]({
message: t("bills.errors.exporting-partner") message: t("bills.errors.exporting-partner")
}); });
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
return; return;
} }
@@ -152,7 +152,7 @@ export function PayableExportAll({
} }
} }
}); });
if (!billUpdateResponse.errors) { if (!!!billUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "billsuccessexport", key: "billsuccessexport",
@@ -187,8 +187,8 @@ export function PayableExportAll({
}); });
await Promise.all(proms); await Promise.all(proms);
if (completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };
@@ -200,7 +200,7 @@ export function PayableExportAll({
); );
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
{t("jobs.actions.exportselected")} {t("jobs.actions.exportselected")}
</Button> </Button>
); );

View File

@@ -46,7 +46,7 @@ export function PayableExportButton({
logImEXEvent("accounting_export_payable"); logImEXEvent("accounting_export_payable");
setLoading(true); setLoading(true);
if (loadingCallback) loadingCallback(true); if (!!loadingCallback) loadingCallback(true);
//Check if it's a QBO Setup. //Check if it's a QBO Setup.
let PartnerResponse; let PartnerResponse;
@@ -88,7 +88,7 @@ export function PayableExportButton({
notification["error"]({ notification["error"]({
message: t("bills.errors.exporting-partner") message: t("bills.errors.exporting-partner")
}); });
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
return; return;
} }
@@ -149,7 +149,7 @@ export function PayableExportButton({
} }
} }
}); });
if (!billUpdateResponse.errors) { if (!!!billUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "billsuccessexport", key: "billsuccessexport",
@@ -186,7 +186,7 @@ export function PayableExportButton({
} }
} }
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };
@@ -198,7 +198,7 @@ export function PayableExportButton({
); );
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.export")} {t("jobs.actions.export")}
</Button> </Button>
); );

View File

@@ -55,7 +55,7 @@ export function PaymentExportButton({
} else { } else {
//Default is QBD //Default is QBD
if (loadingCallback) loadingCallback(true); if (!!loadingCallback) loadingCallback(true);
let QbXmlResponse; let QbXmlResponse;
try { try {
@@ -88,7 +88,7 @@ export function PaymentExportButton({
notification["error"]({ notification["error"]({
message: t("payments.errors.exporting-partner") message: t("payments.errors.exporting-partner")
}); });
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
return; return;
} }
@@ -148,7 +148,7 @@ export function PaymentExportButton({
} }
} }
}); });
if (!paymentUpdateResponse.errors) { if (!!!paymentUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "paymentsuccessexport", key: "paymentsuccessexport",
@@ -184,12 +184,12 @@ export function PaymentExportButton({
) )
]); ]);
} }
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.export")} {t("jobs.actions.export")}
</Button> </Button>
); );

View File

@@ -44,7 +44,7 @@ export function PaymentsExportAllButton({
const handleQbxml = async () => { const handleQbxml = async () => {
setLoading(true); setLoading(true);
if (loadingCallback) loadingCallback(true); if (!!loadingCallback) loadingCallback(true);
let PartnerResponse; let PartnerResponse;
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, { PartnerResponse = await axios.post(`/qbo/payments`, {
@@ -76,7 +76,7 @@ export function PaymentsExportAllButton({
notification["error"]({ notification["error"]({
message: t("payments.errors.exporting-partner") message: t("payments.errors.exporting-partner")
}); });
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
return; return;
} }
@@ -140,7 +140,7 @@ export function PaymentsExportAllButton({
} }
} }
}); });
if (!paymentUpdateResponse.errors) { if (!!!paymentUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "paymentsuccessexport", key: "paymentsuccessexport",
@@ -174,13 +174,13 @@ export function PaymentsExportAllButton({
); );
}); });
await Promise.all(proms); await Promise.all(proms);
if (completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };
return ( return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10} type="primary"> <Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
{t("jobs.actions.exportselected")} {t("jobs.actions.exportselected")}
</Button> </Button>
); );

View File

@@ -207,6 +207,7 @@
- default_prod_list_view - default_prod_list_view
- id - id
- kanban_settings - kanban_settings
- notification_settings
- qbo_realmId - qbo_realmId
- shopid - shopid
- useremail - useremail
@@ -222,6 +223,7 @@
- authlevel - authlevel
- default_prod_list_view - default_prod_list_view
- kanban_settings - kanban_settings
- notification_settings
- qbo_realmId - qbo_realmId
filter: filter:
user: user:
@@ -681,6 +683,31 @@
- 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:
@@ -2805,6 +2832,80 @@
_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
@@ -4326,6 +4427,58 @@
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:
@@ -4669,6 +4822,57 @@
_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
@@ -4909,6 +5113,32 @@
- 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
@@ -5879,6 +6109,36 @@
_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
@@ -6035,6 +6295,32 @@
_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

@@ -0,0 +1,4 @@
-- 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

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

View File

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

View File

@@ -0,0 +1,2 @@
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

@@ -0,0 +1,3 @@
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

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

View File

@@ -0,0 +1,4 @@
-- 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

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

View File

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

View File

@@ -0,0 +1,2 @@
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

@@ -0,0 +1,3 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
-- 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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
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

@@ -0,0 +1,2 @@
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

@@ -0,0 +1,2 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,28 @@
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();
router.use(validateFirebaseIdTokenMiddleware); // These are FCM handlers
router.post("/subscribe", validateFirebaseIdTokenMiddleware, subscribe);
router.post("/unsubscribe", validateFirebaseIdTokenMiddleware, unsubscribe);
router.post("/sendtestnotification", validateFirebaseIdTokenMiddleware, sendNotification);
router.post("/subscribe", subscribe); // Hasura Entry points for creating notifications
router.post("/unsubscribe", unsubscribe); router.post("/events/handleJobsChange", eventAuthorizationMiddleware, handleJobsChange);
router.post("/sendtestnotification", sendNotification); router.post("/events/handleBillsChange", eventAuthorizationMiddleware, handleBillsChange);
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;