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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { useSubscription } from "@apollo/client";
import { SUBSCRIPTION_SCOREBOARD } from "../../graphql/scoreboard.queries"; import { SUBSCRIPTION_SCOREBOARD } from "../../graphql/scoreboard.queries";
import moment from "moment"; import moment from "moment";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -44,9 +45,11 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]); }, [t, setBreadcrumbs, setSelectedHeader]);
return ( return (
<RbacWrapper action="scoreboard:view"> <FeatureWrapper featureName="scoreboard">
<ScoreboardDisplay scoreboardSubscription={scoreboardSubscription} /> <RbacWrapper action="scoreboard:view">
</RbacWrapper> <ScoreboardDisplay scoreboardSubscription={scoreboardSubscription} />
</RbacWrapper>
</FeatureWrapper>
); );
} }
export default connect( 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 TechHeader from "../../components/tech-header/tech-header.component";
import TechSider from "../../components/tech-sider/tech-sider.component"; 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 "./tech.page.styles.scss"; import "./tech.page.styles.scss";
const TimeTicketModalContainer = lazy(() => const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container") import("../../components/time-ticket-modal/time-ticket-modal.container")
@@ -51,52 +52,55 @@ export function TechPage({ technician, match }) {
}, [t]); }, [t]);
return ( return (
<Layout className='tech-layout-container'> <Layout className="tech-layout-container">
<TechSider /> <TechSider />
<Layout> <Layout>
{technician ? null : <Redirect to={`${match.path}/login`} />} {technician ? null : <Redirect to={`${match.path}/login`} />}
<TechHeader /> <TechHeader />
<Content className='tech-content-container'> <Content className="tech-content-container">
<FcmNotification /> <FcmNotification />
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense
fallback={ fallback={
<LoadingSpinner message={t("general.labels.loadingapp")} /> <LoadingSpinner message={t("general.labels.loadingapp")} />
}> }
<TimeTicketModalContainer /> >
<PrintCenterModalContainer /> <FeatureWrapper featureName="tech-console">
<Switch> <TimeTicketModalContainer />
<Route <PrintCenterModalContainer />
exact <Switch>
path={`${match.path}/login`} <Route
component={TechLogin} exact
/> path={`${match.path}/login`}
<Route component={TechLogin}
exact />
path={`${match.path}/joblookup`} <Route
component={TechLookup} exact
/> path={`${match.path}/joblookup`}
<Route component={TechLookup}
exact />
path={`${match.path}/list`} <Route
component={ProductionListPage} exact
/> path={`${match.path}/list`}
<Route component={ProductionListPage}
exact />
path={`${match.path}/jobclock`} <Route
component={TechJobClock} exact
/> path={`${match.path}/jobclock`}
<Route component={TechJobClock}
exact />
path={`${match.path}/shiftclock`} <Route
component={TechShiftClock} exact
/> path={`${match.path}/shiftclock`}
<Route component={TechShiftClock}
exact />
path={`${match.path}/board`} <Route
component={ProductionBoardPage} exact
/> path={`${match.path}/board`}
</Switch> component={ProductionBoardPage}
/>
</Switch>
</FeatureWrapper>
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>

View File

@@ -7,12 +7,18 @@ import { setBodyshop } from "../../redux/user/user.actions";
import TechPage from "./tech.page.component"; import TechPage from "./tech.page.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBodyshop: (bs) => dispatch(setBodyshop(bs)), setBodyshop: (bs) => dispatch(setBodyshop(bs)),
}); });
export function TechPageContainer({ setBodyshop, match }) { export function TechPageContainer({ bodyshop, setBodyshop, match }) {
const { loading, error, data } = useQuery(QUERY_BODYSHOP, { const { loading, error, data } = useQuery(QUERY_BODYSHOP, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
}); });
@@ -21,10 +27,10 @@ export function TechPageContainer({ setBodyshop, match }) {
if (data) setBodyshop(data.bodyshops[0]); if (data) setBodyshop(data.bodyshops[0]);
}, [data, setBodyshop]); }, [data, setBodyshop]);
if (loading) if (loading || !bodyshop)
return <LoadingSpinner message={t("general.labels.loadingshop")} />; return <LoadingSpinner message={t("general.labels.loadingshop")} />;
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return <TechPage match={match} />; 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.", "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", "newversiontitle": "New version of ImEX Online Available",
"noacctfilepath": "There is no accounting file path set. You will not be able to export any items.", "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. ", "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.", "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...", "notfoundtitle": "We couldn't find what you're looking for...",

View File

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

View File

@@ -899,6 +899,7 @@
"newversionmessage": "", "newversionmessage": "",
"newversiontitle": "", "newversiontitle": "",
"noacctfilepath": "", "noacctfilepath": "",
"nofeatureaccess": "",
"noshop": "", "noshop": "",
"notfoundsub": "", "notfoundsub": "",
"notfoundtitle": "", "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 - email
- enforce_class - enforce_class
- enforce_referral - enforce_referral
- features
- federal_tax_id - federal_tax_id
- id - id
- imexshopid - imexshopid
@@ -3596,6 +3597,9 @@ tables:
- name: user - name: user
using: using:
foreign_key_constraint_on: user_email foreign_key_constraint_on: user_email
- name: userByOrderedby
using:
foreign_key_constraint_on: orderedby
- name: vendor - name: vendor
using: using:
foreign_key_constraint_on: vendorid foreign_key_constraint_on: vendorid
@@ -4172,6 +4176,13 @@ tables:
table: table:
schema: public schema: public
name: parts_orders name: parts_orders
- name: partsOrdersByOrderedby
using:
foreign_key_constraint_on:
column: orderedby
table:
schema: public
name: parts_orders
insert_permissions: insert_permissions:
- role: user - role: user
permission: permission: