IO-1211 Feature Restrictions

This commit is contained in:
Patrick Fic
2021-06-23 14:15:41 -07:00
parent d634fcd4cf
commit b49555e111
19 changed files with 373 additions and 49 deletions

View File

@@ -14310,6 +14310,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>nofeatureaccess</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>noshop</name>
<definition_loaded>false</definition_loaded>

View File

@@ -0,0 +1,50 @@
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
function FeatureWrapper({
bodyshop,
featureName,
noauth,
children,
...restProps
}) {
const { t } = useTranslation();
if (HasFeatureAccess({ featureName, bodyshop })) return children;
return (
noauth || (
<AlertComponent
message={t("general.messages.nofeatureaccess")}
type="warning"
/>
)
);
}
export function HasFeatureAccess({ featureName, bodyshop }) {
return (
bodyshop.features.allAccess ||
moment(bodyshop.features[featureName]).isAfter(moment())
);
}
export default connect(mapStateToProps, null)(FeatureWrapper);
/*
dashboard
production-board
scoreboard
csi
tech-console
mobile-imaging
*/

View File

@@ -19,6 +19,7 @@ import {
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser'
@@ -179,6 +180,8 @@ export function JobsDetailHeaderCsi({
}
};
if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return <></>;
return (
<Menu.SubMenu
key="sendcsi"

View File

@@ -90,6 +90,7 @@ export const QUERY_BODYSHOP = gql`
jc_hourly_rates
md_jobline_presets
cdk_dealerid
features
employees {
id
active

View File

@@ -7,6 +7,7 @@ import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -28,9 +29,11 @@ export function ExportsLogPageContainer({ setBreadcrumbs, setSelectedHeader }) {
}, [setBreadcrumbs, t, setSelectedHeader]);
return (
<RbacWrapper action="shop:dashboard">
<DashboardGridComponent />
</RbacWrapper>
<FeatureWrapper featureName="dashboard">
<RbacWrapper action="shop:dashboard">
<DashboardGridComponent />
</RbacWrapper>
</FeatureWrapper>
);
}
export default connect(null, mapDispatchToProps)(ExportsLogPageContainer);

View File

@@ -9,6 +9,7 @@ import {
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ProductionBoardComponent from "./production-board.component";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -38,9 +39,11 @@ export function ProductionBoardContainer({
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<RbacWrapper action="production:board">
<ProductionBoardComponent />
</RbacWrapper>
<FeatureWrapper featureName="production-board">
<RbacWrapper action="production:board">
<ProductionBoardComponent />
</RbacWrapper>
</FeatureWrapper>
);
}
export default connect(

View File

@@ -12,6 +12,7 @@ import { useSubscription } from "@apollo/client";
import { SUBSCRIPTION_SCOREBOARD } from "../../graphql/scoreboard.queries";
import moment from "moment";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -44,9 +45,11 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<RbacWrapper action="scoreboard:view">
<ScoreboardDisplay scoreboardSubscription={scoreboardSubscription} />
</RbacWrapper>
<FeatureWrapper featureName="scoreboard">
<RbacWrapper action="scoreboard:view">
<ScoreboardDisplay scoreboardSubscription={scoreboardSubscription} />
</RbacWrapper>
</FeatureWrapper>
);
}
export default connect(

View File

@@ -10,6 +10,7 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import TechHeader from "../../components/tech-header/tech-header.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import "./tech.page.styles.scss";
const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container")
@@ -51,52 +52,55 @@ export function TechPage({ technician, match }) {
}, [t]);
return (
<Layout className='tech-layout-container'>
<Layout className="tech-layout-container">
<TechSider />
<Layout>
{technician ? null : <Redirect to={`${match.path}/login`} />}
<TechHeader />
<Content className='tech-content-container'>
<Content className="tech-content-container">
<FcmNotification />
<ErrorBoundary>
<Suspense
fallback={
<LoadingSpinner message={t("general.labels.loadingapp")} />
}>
<TimeTicketModalContainer />
<PrintCenterModalContainer />
<Switch>
<Route
exact
path={`${match.path}/login`}
component={TechLogin}
/>
<Route
exact
path={`${match.path}/joblookup`}
component={TechLookup}
/>
<Route
exact
path={`${match.path}/list`}
component={ProductionListPage}
/>
<Route
exact
path={`${match.path}/jobclock`}
component={TechJobClock}
/>
<Route
exact
path={`${match.path}/shiftclock`}
component={TechShiftClock}
/>
<Route
exact
path={`${match.path}/board`}
component={ProductionBoardPage}
/>
</Switch>
}
>
<FeatureWrapper featureName="tech-console">
<TimeTicketModalContainer />
<PrintCenterModalContainer />
<Switch>
<Route
exact
path={`${match.path}/login`}
component={TechLogin}
/>
<Route
exact
path={`${match.path}/joblookup`}
component={TechLookup}
/>
<Route
exact
path={`${match.path}/list`}
component={ProductionListPage}
/>
<Route
exact
path={`${match.path}/jobclock`}
component={TechJobClock}
/>
<Route
exact
path={`${match.path}/shiftclock`}
component={TechShiftClock}
/>
<Route
exact
path={`${match.path}/board`}
component={ProductionBoardPage}
/>
</Switch>
</FeatureWrapper>
</Suspense>
</ErrorBoundary>

View File

@@ -7,12 +7,18 @@ import { setBodyshop } from "../../redux/user/user.actions";
import TechPage from "./tech.page.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { useTranslation } from "react-i18next";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBodyshop: (bs) => dispatch(setBodyshop(bs)),
});
export function TechPageContainer({ setBodyshop, match }) {
export function TechPageContainer({ bodyshop, setBodyshop, match }) {
const { loading, error, data } = useQuery(QUERY_BODYSHOP, {
fetchPolicy: "network-only",
});
@@ -21,10 +27,10 @@ export function TechPageContainer({ setBodyshop, match }) {
if (data) setBodyshop(data.bodyshops[0]);
}, [data, setBodyshop]);
if (loading)
if (loading || !bodyshop)
return <LoadingSpinner message={t("general.labels.loadingshop")} />;
if (error) return <AlertComponent message={error.message} type="error" />;
return <TechPage match={match} />;
}
export default connect(null, mapDispatchToProps)(TechPageContainer);
export default connect(mapStateToProps, mapDispatchToProps)(TechPageContainer);

View File

@@ -899,6 +899,7 @@
"newversionmessage": "Click refresh below to update to the latest available version of ImEX Online. Please make sure all other tabs and windows are closed.",
"newversiontitle": "New version of ImEX Online Available",
"noacctfilepath": "There is no accounting file path set. You will not be able to export any items.",
"nofeatureaccess": "You do not have access to this feature of ImEX Online. Please contact support to request a license for this feature.",
"noshop": "You do not have access to any shops. Please reach out to your shop manager or technical support. ",
"notfoundsub": "Please make sure that you have access to the data or that the link is correct.",
"notfoundtitle": "We couldn't find what you're looking for...",

View File

@@ -899,6 +899,7 @@
"newversionmessage": "",
"newversiontitle": "",
"noacctfilepath": "",
"nofeatureaccess": "",
"noshop": "",
"notfoundsub": "",
"notfoundtitle": "",

View File

@@ -899,6 +899,7 @@
"newversionmessage": "",
"newversiontitle": "",
"noacctfilepath": "",
"nofeatureaccess": "",
"noshop": "",
"notfoundsub": "",
"notfoundtitle": "",

View File

@@ -0,0 +1,12 @@
- args:
relationship: partsOrdersByOrderedby
table:
name: users
schema: public
type: drop_relationship
- args:
relationship: userByOrderedby
table:
name: parts_orders
schema: public
type: drop_relationship

View File

@@ -0,0 +1,20 @@
- args:
name: partsOrdersByOrderedby
table:
name: users
schema: public
using:
foreign_key_constraint_on:
column: orderedby
table:
name: parts_orders
schema: public
type: create_array_relationship
- args:
name: userByOrderedby
table:
name: parts_orders
schema: public
using:
foreign_key_constraint_on: orderedby
type: create_object_relationship

View File

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

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "features" jsonb NULL DEFAULT
jsonb_build_object();
type: run_sql

View File

@@ -0,0 +1,86 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- cdk_dealerid
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- federal_tax_id
- id
- imexshopid
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- jc_hourly_rates
- jobsizelimit
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- phone
- prodtargethrs
- production_config
- region_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- stripe_acct_id
- sub_status
- target_touchtime
- template_header
- textid
- updated_at
- use_fippa
- website
- workingdays
- zip_post
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,87 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_alt_transport
- appt_colors
- appt_length
- bill_tax_rates
- cdk_dealerid
- city
- country
- created_at
- default_adjustment_rate
- deliverchecklist
- email
- enforce_class
- enforce_referral
- features
- federal_tax_id
- id
- imexshopid
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- jc_hourly_rates
- jobsizelimit
- logo_img_path
- md_categories
- md_ccc_rates
- md_classes
- md_hour_split
- md_ins_cos
- md_jobline_presets
- md_labor_rates
- md_messaging_presets
- md_notes_presets
- md_order_statuses
- md_parts_locations
- md_payment_types
- md_rbac
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- phone
- prodtargethrs
- production_config
- region_config
- schedule_end_time
- schedule_start_time
- scoreboard_target
- shopname
- shoprates
- speedprint
- ssbuckets
- state
- state_tax_id
- stripe_acct_id
- sub_status
- target_touchtime
- template_header
- textid
- updated_at
- use_fippa
- website
- workingdays
- zip_post
computed_fields: []
filter:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -766,6 +766,7 @@ tables:
- email
- enforce_class
- enforce_referral
- features
- federal_tax_id
- id
- imexshopid
@@ -3596,6 +3597,9 @@ tables:
- name: user
using:
foreign_key_constraint_on: user_email
- name: userByOrderedby
using:
foreign_key_constraint_on: orderedby
- name: vendor
using:
foreign_key_constraint_on: vendorid
@@ -4172,6 +4176,13 @@ tables:
table:
schema: public
name: parts_orders
- name: partsOrdersByOrderedby
using:
foreign_key_constraint_on:
column: orderedby
table:
schema: public
name: parts_orders
insert_permissions:
- role: user
permission: