Merged in feature/america (pull request #859)

Feature/america
This commit is contained in:
Patrick Fic
2023-06-12 19:42:56 +00:00
30 changed files with 466 additions and 155 deletions

View File

@@ -145,7 +145,7 @@
//Update row highlighting on production board. //Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td { .ant-table-tbody > tr.ant-table-row:hover > td {
background: #eaeaea !important; background: #e7f3ff !important;
} }
.job-line-manual { .job-line-manual {

View File

@@ -1,13 +1,18 @@
import { Badge, List, Tag } from "antd"; import { Badge, List, Tag } from "antd";
import React, { useEffect } from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { List as VirtualizedList, AutoSizer } from "react-virtualized";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
@@ -24,57 +29,66 @@ function ChatConversationListComponent({
conversationList, conversationList,
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
subscribeToMoreConversations,
loadMoreConversations, loadMoreConversations,
}) { }) {
useEffect( const cache = new CellMeasurerCache({
() => subscribeToMoreConversations(), fixedWidth: true,
// eslint-disable-next-line react-hooks/exhaustive-deps defaultHeight: 60,
[] });
);
const rowRenderer = ({ index, key, style }) => { const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index]; const item = conversationList[index];
return ( return (
<List.Item <CellMeasurer
key={key} key={key}
onClick={() => setSelectedConversation(item.id)} cache={cache}
className={`chat-list-item ${ parent={parent}
item.id === selectedConversation columnIndex={0}
? "chat-list-selected-conversation" rowIndex={index}
: null
}`}
style={style}
> >
<div sryle={{ display: "inline-block" }}> <List.Item
{item.label && <div className="chat-name">{item.label}</div>} onClick={() => setSelectedConversation(item.id)}
{item.job_conversations.length > 0 ? ( className={`chat-list-item ${
<div className="chat-name"> item.id === selectedConversation
{item.job_conversations.map((j, idx) => ( ? "chat-list-selected-conversation"
<div key={idx}> : null
<OwnerNameDisplay ownerObject={j.job} /> }`}
</div> style={style}
))} >
</div> <div
) : ( style={{
<PhoneFormatter>{item.phone_num}</PhoneFormatter> display: "inline-block",
)} }}
</div> >
<div sryle={{ display: "inline-block" }}> {item.label && <div className="chat-name">{item.label}</div>}
<div> {item.job_conversations.length > 0 ? (
{item.job_conversations.length > 0 <div className="chat-name">
? item.job_conversations.map((j, idx) => ( {item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag"> <div key={idx}>
{j.job.ro_number} <OwnerNameDisplay ownerObject={j.job} />
</Tag> </div>
)) ))}
: null} </div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div> </div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter> <div style={{ display: "inline-block" }}>
</div> <div>
<Badge count={item.messages_aggregate.aggregate.count || 0} /> {item.job_conversations.length > 0
</List.Item> ? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
); );
}; };
@@ -86,7 +100,7 @@ function ChatConversationListComponent({
height={height} height={height}
width={width} width={width}
rowCount={conversationList.length} rowCount={conversationList.length}
rowHeight={60} rowHeight={cache.rowHeight}
rowRenderer={rowRenderer} rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => { onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) { if (scrollTop + clientHeight === scrollHeight) {

View File

@@ -4,7 +4,7 @@ import {
ShrinkOutlined, ShrinkOutlined,
SyncOutlined, SyncOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useLazyQuery, useSubscription } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -12,8 +12,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import {
CONVERSATION_LIST_QUERY, CONVERSATION_LIST_QUERY,
CONVERSATION_LIST_SUBSCRIPTION, UNREAD_CONVERSATION_COUNT,
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION,
} from "../../graphql/conversations.queries"; } from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import { import {
@@ -42,19 +41,20 @@ export function ChatPopupComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0); const [pollInterval, setpollInterval] = useState(0);
const { data: unreadData } = useSubscription( const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION
);
const [
getConversations,
{ loading, data, called, refetch, fetchMore, subscribeToMore },
] = useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
skip: !chatVisible, ...(pollInterval > 0 ? { pollInterval } : {}),
}); });
const [getConversations, { loading, data, refetch, fetchMore }] =
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
});
const fcmToken = sessionStorage.getItem("fcmtoken"); const fcmToken = sessionStorage.getItem("fcmtoken");
useEffect(() => { useEffect(() => {
@@ -66,13 +66,13 @@ export function ChatPopupComponent({
}, [fcmToken]); }, [fcmToken]);
useEffect(() => { useEffect(() => {
if (called && chatVisible) if (chatVisible)
getConversations({ getConversations({
variables: { variables: {
offset: 0, offset: 0,
}, },
}); });
}, [chatVisible, called, getConversations]); }, [chatVisible, getConversations]);
const loadMoreConversations = useCallback(() => { const loadMoreConversations = useCallback(() => {
if (data) if (data)
@@ -119,43 +119,6 @@ export function ChatPopupComponent({
<ChatConversationListComponent <ChatConversationListComponent
conversationList={data ? data.conversations : []} conversationList={data ? data.conversations : []}
loadMoreConversations={loadMoreConversations} loadMoreConversations={loadMoreConversations}
subscribeToMoreConversations={() =>
subscribeToMore({
document: CONVERSATION_LIST_SUBSCRIPTION,
variables: { offset: 0 },
updateQuery: (prev, { subscriptionData }) => {
if (
!subscriptionData.data ||
subscriptionData.data.conversations.length === 0
)
return prev;
let conversations = [...prev.conversations];
const newConversations =
subscriptionData.data.conversations;
for (const conversation of newConversations) {
const index = conversations.findIndex(
(prevConversation) =>
prevConversation.id === conversation.id
);
if (index !== -1) {
conversations.splice(index, 1);
conversations.unshift(conversation);
continue;
}
conversations.unshift(conversation);
}
return Object.assign({}, prev, {
conversations: conversations,
});
},
})
}
/> />
)} )}
</Col> </Col>

View File

@@ -66,7 +66,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
key: "status", key: "status",
}, },
{ {
title: t("jobs.fields.ro_number"), title: t("bills.fields.invoice_number"),
dataIndex: ["Posting", "Reference"], dataIndex: ["Posting", "Reference"],
key: "reference", key: "reference",
}, },

View File

@@ -334,7 +334,9 @@ function Header({
icon={<SettingOutlined />} icon={<SettingOutlined />}
> >
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}> <Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link> <Link to="/manage/shop?tab=info">
{t("menus.header.shop_config")}
</Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}> <Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link> <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>

View File

@@ -53,7 +53,9 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
<Tabs <Tabs
defaultActiveKey={search.subtab} defaultActiveKey={search.subtab}
onChange={(key) => onChange={(key) =>
history.push({ search: `?tab=${search.tab}&subtab=${key}` }) history.push({
search: `?tab=${search.tab}&subtab=${key}`,
})
} }
> >
<Tabs.TabPane key="general" tab={t("bodyshop.labels.shopinfo")}> <Tabs.TabPane key="general" tab={t("bodyshop.labels.shopinfo")}>

View File

@@ -0,0 +1,78 @@
import { Alert, Button, Col, Row, Space } from "antd";
import i18n from "i18next";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { AlertOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { setUpdateAvailable } from "../../redux/application/application.actions";
import { store } from "../../redux/store";
import * as serviceWorkerRegistration from "../../serviceWorkerRegistration";
let globalRegistration;
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(UpdateAlert);
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
if (!updateAvailable) return null;
return (
<Alert
message={t("general.messages.newversiontitle")}
showIcon
icon={<AlertOutlined />}
description={
<Row gutter={[16, 16]}>
<Col sm={24} md={16} lg={18}>
{t("general.messages.newversionmessage")}
</Col>
<Col sm={24} md={8} lg={6}>
<Space wrap>
<Button
onClick={async () => {
window.open("https://imex-online.noticeable.news/", "_blank");
}}
>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button
type="primary"
onClick={async () => {
if (globalRegistration && globalRegistration.waiting) {
await globalRegistration.unregister();
// Makes Workbox call skipWaiting()
globalRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
// Once the service worker is unregistered, we can reload the page to let
// the browser download a fresh copy of our app (invalidating the cache)
window.location.reload();
}
}}
>
{i18n.t("general.actions.refresh")}
</Button>
</Space>
</Col>
</Row>
}
closable={false}
type="warning"
/>
);
}
const onServiceWorkerUpdate = (registration) => {
console.log("onServiceWorkerUpdate", registration);
globalRegistration = registration;
store.dispatch(setUpdateAvailable(true));
};
serviceWorkerRegistration.register({ onUpdate: onServiceWorkerUpdate });

View File

@@ -1,37 +1,5 @@
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
export const CONVERSATION_LIST_SUBSCRIPTION = gql`
subscription CONVERSATION_LIST_SUBSCRIPTION($offset: Int!) {
conversations(
order_by: { updated_at: desc }
limit: 1
offset: $offset
where: { archived: { _eq: false } }
) {
phone_num
id
updated_at
unreadcnt
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
job_conversations {
job {
id
ro_number
ownr_fn
ownr_ln
ownr_co_nm
}
}
}
}
`;
export const UNREAD_CONVERSATION_COUNT = gql` export const UNREAD_CONVERSATION_COUNT = gql`
query UNREAD_CONVERSATION_COUNT { query UNREAD_CONVERSATION_COUNT {
messages_aggregate( messages_aggregate(
@@ -44,18 +12,6 @@ export const UNREAD_CONVERSATION_COUNT = gql`
} }
`; `;
export const UNREAD_CONVERSATION_COUNT_SUBSCRIPTION = gql`
subscription UNREAD_CONVERSATION_COUNT_SUBSCRIPTION {
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
}
`;
export const CONVERSATION_LIST_QUERY = gql` export const CONVERSATION_LIST_QUERY = gql`
query CONVERSATION_LIST_QUERY($offset: Int!) { query CONVERSATION_LIST_QUERY($offset: Int!) {
conversations( conversations(

View File

@@ -25,6 +25,7 @@ import {
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import "./manage.page.styles.scss"; import "./manage.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component";
const ManageRootPage = lazy(() => const ManageRootPage = lazy(() =>
import("../manage-root/manage-root.page.container") import("../manage-root/manage-root.page.container")
@@ -410,6 +411,7 @@ export function Manage({ match, conflict, bodyshop }) {
<> <>
<ChatAffixContainer /> <ChatAffixContainer />
<Layout className="layout-container"> <Layout className="layout-container">
<UpdateAlert />
<HeaderContainer /> <HeaderContainer />
<Content className="content-container"> <Content className="content-container">

View File

@@ -6,7 +6,7 @@ import AlertComponent from "../../components/alert/alert.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { setBodyshop } from "../../redux/user/user.actions"; import { setBodyshop } from "../../redux/user/user.actions";
import "../../utils/RegisterSw"; //import "../../utils/RegisterSw";
import ManagePage from "./manage.page.component"; import ManagePage from "./manage.page.component";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({

View File

@@ -42,6 +42,10 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
]); ]);
}, [t, setSelectedHeader, setBreadcrumbs, bodyshop.shopname]); }, [t, setSelectedHeader, setBreadcrumbs, bodyshop.shopname]);
useEffect(() => {
if (!search.tab) history.push({ search: "?tab=info" });
}, [history, search]);
return ( return (
<RbacWrapper action="shop:config"> <RbacWrapper action="shop:config">
<Tabs <Tabs

View File

@@ -12,6 +12,7 @@ import TechSider from "../../components/tech-sider/tech-sider.component";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component"; import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import "./tech.page.styles.scss"; import "./tech.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component";
const TimeTicketModalContainer = lazy(() => const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container") import("../../components/time-ticket-modal/time-ticket-modal.container")
); );
@@ -56,7 +57,9 @@ export function TechPage({ technician, match }) {
<TechSider /> <TechSider />
<Layout> <Layout>
{technician ? null : <Redirect to={`${match.path}/login`} />} {technician ? null : <Redirect to={`${match.path}/login`} />}
<UpdateAlert />
<TechHeader /> <TechHeader />
<Content className="tech-content-container"> <Content className="tech-content-container">
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense

View File

@@ -9,7 +9,7 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import "../../utils/RegisterSw"; //import "../../utils/RegisterSw";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,

View File

@@ -62,3 +62,8 @@ export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS, type: ApplicationActionTypes.SET_PROBLEM_JOBS,
payload: problemJobs, payload: problemJobs,
}); });
export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable,
});

View File

@@ -3,6 +3,7 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = { const INITIAL_STATE = {
loading: false, loading: false,
online: true, online: true,
updateAvailable: false,
breadcrumbs: [], breadcrumbs: [],
recentItems: [], recentItems: [],
selectedHeader: "home", selectedHeader: "home",
@@ -18,6 +19,11 @@ const INITIAL_STATE = {
const applicationReducer = (state = INITIAL_STATE, action) => { const applicationReducer = (state = INITIAL_STATE, action) => {
switch (action.type) { switch (action.type) {
case ApplicationActionTypes.SET_UPDATE_AVAILABLE:
return {
...state,
updateAvailable: action.payload,
};
case ApplicationActionTypes.SET_SELECTED_HEADER: case ApplicationActionTypes.SET_SELECTED_HEADER:
return { return {
...state, ...state,

View File

@@ -48,3 +48,7 @@ export const selectProblemJobs = createSelector(
[selectApplication], [selectApplication],
(application) => application.problemJobs (application) => application.problemJobs
); );
export const selectUpdateAvailable = createSelector(
[selectApplication],
(application) => application.updateAvailable
);

View File

@@ -12,5 +12,6 @@ const ApplicationActionTypes = {
SET_ONLINE_STATUS: "SET_ONLINE_STATUS", SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL", INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS", SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
}; };
export default ApplicationActionTypes; export default ApplicationActionTypes;

View File

@@ -3,6 +3,7 @@ import { Button, notification, Space } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import React from "react"; import React from "react";
import * as serviceWorkerRegistration from "../serviceWorkerRegistration"; import * as serviceWorkerRegistration from "../serviceWorkerRegistration";
import { store } from "../redux/store";
const onServiceWorkerUpdate = (registration) => { const onServiceWorkerUpdate = (registration) => {
console.log("onServiceWorkerUpdate", registration); console.log("onServiceWorkerUpdate", registration);
@@ -33,6 +34,9 @@ const onServiceWorkerUpdate = (registration) => {
</Button> </Button>
</Space> </Space>
); );
store.dispatch()
notification.open({ notification.open({
icon: <AlertOutlined />, icon: <AlertOutlined />,
message: i18n.t("general.messages.newversiontitle"), message: i18n.t("general.messages.newversiontitle"),

View File

@@ -2690,6 +2690,13 @@
table: table:
name: inventory name: inventory
schema: public schema: public
- name: parts_dispatch_lines
using:
foreign_key_constraint_on:
column: joblineid
table:
name: parts_dispatch_lines
schema: public
- name: parts_order_lines - name: parts_order_lines
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -3131,6 +3138,13 @@
table: table:
name: notes name: notes
schema: public schema: public
- name: parts_dispatches
using:
foreign_key_constraint_on:
column: jobid
table:
name: parts_dispatch
schema: public
- name: parts_orders - name: parts_orders
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -4556,6 +4570,165 @@
template_engine: Kriti template_engine: Kriti
url: '{{$base_url}}/opensearch' url: '{{$base_url}}/opensearch'
version: 2 version: 2
- table:
name: parts_dispatch
schema: public
object_relationships:
- name: job
using:
foreign_key_constraint_on: jobid
array_relationships:
- name: parts_dispatch_lines
using:
foreign_key_constraint_on:
column: partsdispatchid
table:
name: parts_dispatch_lines
schema: public
insert_permissions:
- role: user
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- jobid
- number
- employeeid
- dispatched_at
- dispatched_by
select_permissions:
- role: user
permission:
columns:
- number
- dispatched_by
- created_at
- dispatched_at
- updated_at
- employeeid
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
update_permissions:
- role: user
permission:
columns:
- number
- dispatched_by
- created_at
- dispatched_at
- updated_at
- employeeid
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table:
name: parts_dispatch_lines
schema: public
object_relationships:
- name: jobline
using:
foreign_key_constraint_on: joblineid
- name: parts_dispatch
using:
foreign_key_constraint_on: partsdispatchid
insert_permissions:
- role: user
permission:
check:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- partsdispatchid
- joblineid
- quantity
- accepted_at
select_permissions:
- role: user
permission:
columns:
- quantity
- accepted_at
- created_at
- updated_at
- id
- joblineid
- partsdispatchid
filter:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
update_permissions:
- role: user
permission:
columns:
- id
- created_at
- updated_at
- partsdispatchid
- joblineid
- quantity
- accepted_at
filter:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table: - table:
name: parts_order_lines name: parts_order_lines
schema: public schema: public

View File

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

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."parts_dispatch" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "jobid" uuid NOT NULL, "number" serial NOT NULL, "employeeid" uuid NOT NULL, "dispatched_at" timestamptz NOT NULL, "dispatched_by" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_parts_dispatch_updated_at"
BEFORE UPDATE ON "public"."parts_dispatch"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_parts_dispatch_updated_at" ON "public"."parts_dispatch"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

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

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."parts_dispatch_lines" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "partsdispatchid" UUID NOT NULL, "joblineid" uuid NOT NULL, "quantity" numeric NOT NULL DEFAULT 1, "accepted_at" timestamptz NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("joblineid") REFERENCES "public"."joblines"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_parts_dispatch_lines_updated_at"
BEFORE UPDATE ON "public"."parts_dispatch_lines"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_parts_dispatch_lines_updated_at" ON "public"."parts_dispatch_lines"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
alter table "public"."parts_dispatch_lines" drop constraint "parts_dispatch_lines_partsdispatchid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."parts_dispatch_lines"
add constraint "parts_dispatch_lines_partsdispatchid_fkey"
foreign key ("partsdispatchid")
references "public"."parts_dispatch"
("id") on update cascade on delete cascade;

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
alter table "public"."job_conversations" drop constraint "job_conversations_conversationid_fkey",
add constraint "job_conversations_conversationid_fkey"
foreign key ("conversationid")
references "public"."conversations"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."job_conversations" drop constraint "job_conversations_conversationid_fkey",
add constraint "job_conversations_conversationid_fkey"
foreign key ("conversationid")
references "public"."conversations"
("id") on update cascade on delete cascade;

View File

@@ -86,6 +86,7 @@ async function OpenSearchUpdateHandler(req, res) {
"v_model_yr", "v_model_yr",
"v_make_desc", "v_make_desc",
"v_model_desc", "v_model_desc",
"v_vin",
]); ]);
document.bodyshopid = req.body.event.data.new.shopid; document.bodyshopid = req.body.event.data.new.shopid;
break; break;
@@ -139,7 +140,7 @@ async function OpenSearchUpdateHandler(req, res) {
"exported_at", "exported_at",
"invoice_number", "invoice_number",
"is_credit_memo", "is_credit_memo",
"total" "total",
]), ]),
...bill.bills_by_pk, ...bill.bills_by_pk,
bodyshopid: bill.bills_by_pk.job.shopid, bodyshopid: bill.bills_by_pk.job.shopid,
@@ -244,17 +245,54 @@ async function OpensearchSearchHandler(req, res) {
bool: { bool: {
must: [ must: [
{ {
multi_match: { match: {
query: search, bodyshopid: assocs.associations[0].shopid,
type: "phrase_prefix",
//fields: ["*"],
// fuzziness: "5",
//prefix_length: 2,
}, },
}, },
{ {
match: { bool: {
bodyshopid: assocs.associations[0].shopid, should: [
{
multi_match: {
query: search,
type: "cross_fields",
fields: ["*ownr_fn", "*ownr_ln"],
},
},
{
multi_match: {
query: search,
type: "most_fields",
fields: [
"*v_model_yr",
"*v_make_desc^2",
"*v_model_desc^3",
],
},
},
{
query_string: {
query: `*${search}*`,
fields: [
"*ro_number^20",
"*clm_no^14",
"*v_vin^12",
"*plate_no^12",
"*ownr_ln^10",
"transactionid^10",
"paymentnum^10",
"invoice_number^10",
"*ownr_fn^8",
"*ownr_co_nm^8",
"*ownr_ph1^8",
"*ownr_ph2^8",
"*",
],
},
},
],
minimum_should_match: 1,
}, },
}, },
], ],