IO-3020 IO-3036 Additional blurred components.

This commit is contained in:
Patrick Fic
2024-12-04 15:37:08 -08:00
parent d9c9466953
commit 43b1ad78a3
7 changed files with 192 additions and 107 deletions

View File

@@ -4,6 +4,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { HasFeatureAccess } from "./feature-wrapper.component"; import { HasFeatureAccess } from "./feature-wrapper.component";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -31,6 +32,7 @@ export function BlurWrapper({
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
if (!ValidateFeatureName(featureName)) console.trace("*** INVALID FEATURE NAME", featureName); if (!ValidateFeatureName(featureName)) console.trace("*** INVALID FEATURE NAME", featureName);
} }
if (debug) { if (debug) {
console.trace("*** DEBUG MODE", featureName); console.trace("*** DEBUG MODE", featureName);
console.log("*** HAS FEATURE ACCESS?", featureName, HasFeatureAccess({ featureName, bodyshop })); console.log("*** HAS FEATURE ACCESS?", featureName, HasFeatureAccess({ featureName, bodyshop }));
@@ -61,8 +63,17 @@ export function BlurWrapper({
} else { } else {
if (typeof overrideValueFunction === "function") { if (typeof overrideValueFunction === "function") {
newValueProp = overrideValueFunction(); newValueProp = overrideValueFunction();
} else if (overrideValueFunction === "RandomDinero") { } else if (typeof overrideValueFunction === "string" && overrideValueFunction === "RandomDinero") {
newValueProp = RandomDinero(); newValueProp = RandomDinero();
} else if (typeof overrideValueFunction === "string" && overrideValueFunction === "RandomAmount") {
newValueProp = RandomAmount();
} else if (
typeof overrideValueFunction === "string" &&
overrideValueFunction.startsWith("RandomSmallString")
) {
newValueProp = RandomSmallString(overrideValueFunction.split(":")[1] || 3); //Default back to 3 words, otherwise use the string.
} else if (typeof overrideValueFunction === "string" && overrideValueFunction.startsWith("RandomDate")) {
newValueProp = RandomDate();
} else { } else {
newValueProp = "This is some random text. Nothing interesting here."; newValueProp = "This is some random text. Nothing interesting here.";
} }
@@ -86,6 +97,23 @@ export default connect(mapStateToProps, null)(BlurWrapper);
function RandomDinero() { function RandomDinero() {
return Dinero({ amount: Math.round(Math.exp(Math.random() * 10, 2)) }).toFormat(); return Dinero({ amount: Math.round(Math.exp(Math.random() * 10, 2)) }).toFormat();
} }
function RandomAmount() {
return Math.round(Math.exp(Math.random() * 10));
}
function RandomSmallString(maxWords = 3) {
const words = ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit"];
const wordCount = Math.floor(Math.random() * maxWords) + 1; // Random number between 1 and 3
let result = [];
for (let i = 0; i < wordCount; i++) {
const randomIndex = Math.floor(Math.random() * words.length);
result.push(words[randomIndex]);
}
return result.join(" ");
}
function RandomDate() {
return DateTimeFormatterFunction(new Date(Math.floor(Math.random() * 1000000000000)));
}
const featureNameList = [ const featureNameList = [
"mobile", "mobile",
@@ -104,9 +132,10 @@ const featureNameList = [
"checklist", "checklist",
"smartscheduling", "smartscheduling",
"roguard", "roguard",
"dashboard" "dashboard",
"lifecycle"
]; ];
function ValidateFeatureName(featureName) { export function ValidateFeatureName(featureName) {
return featureNameList.includes(featureName); return featureNameList.includes(featureName);
} }

View File

@@ -1,11 +1,12 @@
import dayjs from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import { ValidateFeatureName } from "./blur-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -21,9 +22,12 @@ function FeatureWrapper({
...restProps ...restProps
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
if (import.meta.env.DEV) {
if (!ValidateFeatureName(featureName)) console.trace("*** INVALID FEATURE NAME", featureName);
}
if (upsellComponent) { if (upsellComponent) {
console.error("Upsell component passed in. This is not yet implemented."); console.error("*** Upsell component passed in. This is not yet implemented.");
} }
if (HasFeatureAccess({ featureName, bodyshop })) return children; if (HasFeatureAccess({ featureName, bodyshop })) return children;

View File

@@ -8,6 +8,7 @@ import { isEmpty } from "lodash";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "./job-lifecycle.styles.scss"; import "./job-lifecycle.styles.scss";
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
// show text on bar if text can fit // show text on bar if text can fit
export function JobLifecycleComponent({ job, statuses, ...rest }) { export function JobLifecycleComponent({ job, statuses, ...rest }) {
@@ -65,14 +66,23 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
{ {
title: t("job_lifecycle.columns.value"), title: t("job_lifecycle.columns.value"),
dataIndex: "value", dataIndex: "value",
key: "value" key: "value",
render: (text, record) => (
<BlurWrapperComponent featureName="lifecycle" valueProp="children" overrideValueFunction="RandomSmallString:2">
<span>{text}</span>
</BlurWrapperComponent>
)
}, },
{ {
title: t("job_lifecycle.columns.start"), title: t("job_lifecycle.columns.start"),
dataIndex: "start", dataIndex: "start",
key: "start", key: "start",
render: (text) => DateTimeFormatterFunction(text), sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix() render: (text, record) => (
<BlurWrapperComponent featureName="lifecycle" valueProp="children" overrideValueFunction="RandomDate">
<span>{DateTimeFormatterFunction(text)}</span>
</BlurWrapperComponent>
)
}, },
{ {
title: t("job_lifecycle.columns.relative_start"), title: t("job_lifecycle.columns.relative_start"),
@@ -92,7 +102,12 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
} }
return dayjs(a.end).unix() - dayjs(b.end).unix(); return dayjs(a.end).unix() - dayjs(b.end).unix();
}, },
render: (text) => (isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text))
render: (text, record) => (
<BlurWrapperComponent featureName="lifecycle" valueProp="children" overrideValueFunction="RandomDate">
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
</BlurWrapperComponent>
)
}, },
{ {
title: t("job_lifecycle.columns.relative_end"), title: t("job_lifecycle.columns.relative_end"),
@@ -122,67 +137,72 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
} }
style={{ width: "100%" }} style={{ width: "100%" }}
> >
<div {
id="bar-container" //TODO:Upsell
style={{ }
display: "flex", <BlurWrapperComponent featureName="lifecycle">
width: "100%", <div
height: "100px", id="bar-container"
textAlign: "center", style={{
borderRadius: "5px", display: "flex",
borderWidth: "5px", width: "100%",
borderStyle: "solid", height: "100px",
borderColor: "#f0f2f5", textAlign: "center",
margin: 0, borderRadius: "5px",
padding: 0 borderWidth: "5px",
}} borderStyle: "solid",
> borderColor: "#f0f2f5",
{lifecycleData.durations.summations.map((key, index, array) => { margin: 0,
const isFirst = index === 0; padding: 0
const isLast = index === array.length - 1; }}
return ( >
<div {lifecycleData.durations.summations.map((key, index, array) => {
key={key.status} const isFirst = index === 0;
style={{ const isLast = index === array.length - 1;
overflow: "hidden", return (
display: "flex", <div
flexDirection: "column", key={key.status}
justifyContent: "center", style={{
alignItems: "center", overflow: "hidden",
margin: 0, display: "flex",
padding: 0, flexDirection: "column",
justifyContent: "center",
alignItems: "center",
margin: 0,
padding: 0,
borderTop: "1px solid #f0f2f5", borderTop: "1px solid #f0f2f5",
borderBottom: "1px solid #f0f2f5", borderBottom: "1px solid #f0f2f5",
borderLeft: isFirst ? "1px solid #f0f2f5" : undefined, borderLeft: isFirst ? "1px solid #f0f2f5" : undefined,
borderRight: isLast ? "1px solid #f0f2f5" : undefined, borderRight: isLast ? "1px solid #f0f2f5" : undefined,
backgroundColor: key.color, backgroundColor: key.color,
width: `${key.percentage}%` width: `${key.percentage}%`
}} }}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`} aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`} title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
> >
{key.percentage > 15 ? ( {key.percentage > 15 ? (
<> <>
<div>{key.roundedPercentage}</div> <div>{key.roundedPercentage}</div>
<div <div
style={{ style={{
backgroundColor: "#f0f2f5", backgroundColor: "#f0f2f5",
borderRadius: "5px", borderRadius: "5px",
paddingRight: "2px", paddingRight: "2px",
paddingLeft: "2px", paddingLeft: "2px",
fontSize: "0.8rem" fontSize: "0.8rem"
}} }}
> >
{key.status} {key.status}
</div> </div>
</> </>
) : null} ) : null}
</div> </div>
); );
})} })}
</div> </div>
</BlurWrapperComponent>
<Card type="inner" title={t("job_lifecycle.content.legend_title")} style={{ marginTop: "10px" }}> <Card type="inner" title={t("job_lifecycle.content.legend_title")} style={{ marginTop: "10px" }}>
<div> <div>
{lifecycleData.durations.summations.map((key) => ( {lifecycleData.durations.summations.map((key) => (
@@ -197,7 +217,15 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
textAlign: "center" textAlign: "center"
}} }}
> >
{key.status} ({key.roundedPercentage}) {key.status} (
<BlurWrapperComponent
featureName="lifecycle"
overrideValueFunction="RandomAmount"
valueProp="children"
>
<span>{key.roundedPercentage}</span>
</BlurWrapperComponent>
)
</div> </div>
</Tag> </Tag>
))} ))}

View File

@@ -729,15 +729,16 @@ export function JobsDetailHeaderActions({
onClick: () => { onClick: () => {
logImEXEvent("job_header_enter_time_ticekts"); logImEXEvent("job_header_enter_time_ticekts");
setTimeTicketContext({ HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
actions: {}, setTimeTicketContext({
context: { actions: {},
jobId: job.id, context: {
created_by: currentUser.displayName jobId: job.id,
? currentUser.email.concat(" | ", currentUser.displayName) created_by: currentUser.displayName
: currentUser.email ? currentUser.email.concat(" | ", currentUser.displayName)
} : currentUser.email
}); }
});
} }
} }
]; ];
@@ -765,10 +766,11 @@ export function JobsDetailHeaderActions({
onClick: () => { onClick: () => {
logImEXEvent("job_header_enter_payment"); logImEXEvent("job_header_enter_payment");
setPaymentContext({ HasFeatureAccess({ featureName: "payments", bodyshop }) &&
actions: {}, setPaymentContext({
context: { jobid: job.id } actions: {},
}); context: { jobid: job.id }
});
} }
}); });

View File

@@ -12,6 +12,7 @@ import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/appli
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import JobchecklistComponent from "../../components/job-checklist/job-checklist.component"; import JobchecklistComponent from "../../components/job-checklist/job-checklist.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -59,15 +60,24 @@ export function JobsDeliverContainer({ bodyshop, setBreadcrumbs, setSelectedHead
if (data && !!!data.bodyshops_by_pk.deliverchecklist) if (data && !!!data.bodyshops_by_pk.deliverchecklist)
return <AlertComponent message={t("deliver.errors.nochecklist")} type="error" />; return <AlertComponent message={t("deliver.errors.nochecklist")} type="error" />;
return ( return (
<RbacWrapper action="jobs:deliver"> <FeatureWrapperComponent
<div> featureName="checklist"
<JobchecklistComponent upsellComponent={
type="deliver" {
checklistConfig={(data && data.bodyshops_by_pk.deliverchecklist) || {}} //TODO:Upsell
job={data ? data.jobs_by_pk : {}} }
/> }
</div> >
</RbacWrapper> <RbacWrapper action="jobs:deliver">
<div>
<JobchecklistComponent
type="deliver"
checklistConfig={(data && data.bodyshops_by_pk.deliverchecklist) || {}}
job={data ? data.jobs_by_pk : {}}
/>
</div>
</RbacWrapper>
</FeatureWrapperComponent>
); );
} }

View File

@@ -402,7 +402,9 @@ export function JobsDetailPage({
key: "lifecycle", key: "lifecycle",
icon: <BarsOutlined />, icon: <BarsOutlined />,
id: "job-details-lifecycle", id: "job-details-lifecycle",
label: t("menus.jobsdetail.lifecycle"), label: (
<LockWrapperComponent featureName="lifecycle">{t("menus.jobsdetail.lifecycle")}</LockWrapperComponent>
),
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} /> children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} />
}, },
{ {

View File

@@ -13,6 +13,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { Result } from "antd"; import { Result } from "antd";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -63,20 +64,29 @@ export function JobsIntakeContainer({ bodyshop, setBreadcrumbs, setSelectedHeade
return <AlertComponent message={t("intake.errors.nochecklist")} type="error" />; return <AlertComponent message={t("intake.errors.nochecklist")} type="error" />;
return ( return (
<RbacWrapper action="jobs:intake"> <FeatureWrapperComponent
<div> featureName="checklist"
{!!data.jobs_by_pk.intakechecklist || upsellComponent={
!bodyshop.md_ro_statuses.pre_production_statuses.includes(data.jobs_by_pk.status) ? ( {
<Result status="warning" title={t("jobs.errors.cannotintake")} /> //TODO:Upsell
) : ( }
<JobChecklist }
type="intake" >
checklistConfig={(data && data.bodyshops_by_pk.intakechecklist) || {}} <RbacWrapper action="jobs:intake">
job={data && data.jobs_by_pk} <div>
/> {!!data.jobs_by_pk.intakechecklist ||
)} !bodyshop.md_ro_statuses.pre_production_statuses.includes(data.jobs_by_pk.status) ? (
</div> <Result status="warning" title={t("jobs.errors.cannotintake")} />
</RbacWrapper> ) : (
<JobChecklist
type="intake"
checklistConfig={(data && data.bodyshops_by_pk.intakechecklist) || {}}
job={data && data.jobs_by_pk}
/>
)}
</div>
</RbacWrapper>
</FeatureWrapperComponent>
); );
} }