IO-3020 IO-3036 Additional blurred components.
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user