fix: added more fields in lead and deal page & listview
This commit is contained in:
parent
66420a1444
commit
1154bce89c
@ -39,6 +39,8 @@ def get_activities(doc, docinfo):
|
||||
|
||||
for version in docinfo.versions:
|
||||
data = json.loads(version.data)
|
||||
if not data.get("changed"):
|
||||
continue
|
||||
if change := data.get("changed")[0]:
|
||||
activity_type = "changed"
|
||||
field_label = next((f.label for f in lead_fields_meta if f.fieldname == change[0]), None)
|
||||
|
||||
@ -19,9 +19,14 @@
|
||||
</Autocomplete>
|
||||
<Dropdown :options="statusDropdownOptions(deal.data, 'deal')">
|
||||
<template #default="{ open }">
|
||||
<Button :label="deal.data.deal_status">
|
||||
<Button
|
||||
:label="deal.data.deal_status"
|
||||
:class="dealStatuses[deal.data.deal_status].bgColor"
|
||||
>
|
||||
<template #prefix>
|
||||
<IndicatorIcon :class="dealStatuses[deal.data.deal_status].color" />
|
||||
<IndicatorIcon
|
||||
:class="dealStatuses[deal.data.deal_status].color"
|
||||
/>
|
||||
</template>
|
||||
<template #suffix
|
||||
><FeatherIcon
|
||||
@ -31,7 +36,6 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button icon="more-horizontal" />
|
||||
<Button label="Save" variant="solid" @click="() => updateDeal()" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
@ -80,10 +84,11 @@
|
||||
>
|
||||
<Avatar
|
||||
size="3xl"
|
||||
:label="deal.data.first_name"
|
||||
:image="deal.data.image"
|
||||
shape="square"
|
||||
:label="deal.data.organization_name"
|
||||
:image="deal.data.organization_logo"
|
||||
/>
|
||||
<div class="font-medium text-2xl">{{ deal.data.lead_name }}</div>
|
||||
<div class="font-medium text-2xl">{{ deal.data.organization_name }}</div>
|
||||
<div class="flex gap-3">
|
||||
<Tooltip text="Make a call...">
|
||||
<Button
|
||||
@ -96,7 +101,13 @@
|
||||
<Button class="rounded-full h-8 w-8">
|
||||
<EmailIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button icon="message-square" class="rounded-full h-8 w-8" />
|
||||
<Tooltip text="Go to website...">
|
||||
<Button
|
||||
icon="link"
|
||||
@click="openWebsite(deal.data.website)"
|
||||
class="rounded-full h-8 w-8"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button icon="more-horizontal" class="rounded-full h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
@ -218,6 +229,24 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'date'"
|
||||
type="date"
|
||||
v-model="deal.data[field.name]"
|
||||
class="form-control"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'number'"
|
||||
type="number"
|
||||
v-model="deal.data[field.name]"
|
||||
class="form-control"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'tel'"
|
||||
type="tel"
|
||||
v-model="deal.data[field.name]"
|
||||
class="form-control"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
@ -264,7 +293,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import CommunicationArea from '@/components/CommunicationArea.vue'
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
||||
import { dateFormat, timeAgo, dateTooltipFormat, dealStatuses, statusDropdownOptions } from '@/utils'
|
||||
import {
|
||||
dateFormat,
|
||||
timeAgo,
|
||||
dateTooltipFormat,
|
||||
dealStatuses,
|
||||
statusDropdownOptions,
|
||||
openWebsite,
|
||||
} from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import {
|
||||
createResource,
|
||||
@ -315,7 +351,7 @@ function updateDeal() {
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Deals', route: { name: 'Deals' } }]
|
||||
items.push({
|
||||
label: deal.data.lead_name,
|
||||
label: deal.data.organization_name,
|
||||
route: { name: 'Deal', params: { dealId: deal.data.name } },
|
||||
})
|
||||
return items
|
||||
@ -345,11 +381,11 @@ const tabs = computed(() => {
|
||||
),
|
||||
activityTitle: 'Calls',
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
activityTitle: 'Tasks',
|
||||
},
|
||||
// {
|
||||
// label: 'Tasks',
|
||||
// icon: TaskIcon,
|
||||
// activityTitle: 'Tasks',
|
||||
// },
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
@ -376,20 +412,9 @@ function onTabChange(index) {
|
||||
const detailSections = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'About this deal',
|
||||
label: 'Organization',
|
||||
opened: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
name: 'deal_status',
|
||||
options: statusDropdownOptions(deal.data, 'deal'),
|
||||
},
|
||||
{
|
||||
label: 'Deal owner',
|
||||
type: 'link',
|
||||
name: 'lead_owner',
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
type: 'data',
|
||||
@ -400,10 +425,30 @@ const detailSections = computed(() => {
|
||||
type: 'data',
|
||||
name: 'website',
|
||||
},
|
||||
{
|
||||
label: 'Amount',
|
||||
type: 'number',
|
||||
name: 'annual_revenue',
|
||||
},
|
||||
{
|
||||
label: 'Close date',
|
||||
type: 'date',
|
||||
name: 'close_date',
|
||||
},
|
||||
{
|
||||
label: 'Probability',
|
||||
type: 'data',
|
||||
name: 'probability',
|
||||
},
|
||||
{
|
||||
label: 'Next step',
|
||||
type: 'data',
|
||||
name: 'next_step',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Person',
|
||||
label: 'Contacts',
|
||||
opened: true,
|
||||
fields: [
|
||||
{
|
||||
@ -423,7 +468,7 @@ const detailSections = computed(() => {
|
||||
},
|
||||
{
|
||||
label: 'Mobile no.',
|
||||
type: 'phone',
|
||||
type: 'tel',
|
||||
name: 'mobile_no',
|
||||
},
|
||||
],
|
||||
|
||||
@ -102,11 +102,9 @@ const leads = createListResource({
|
||||
doctype: 'CRM Lead',
|
||||
fields: [
|
||||
'name',
|
||||
'first_name',
|
||||
'lead_name',
|
||||
'image',
|
||||
'organization_name',
|
||||
'organization_logo',
|
||||
'annual_revenue',
|
||||
'deal_status',
|
||||
'email',
|
||||
'mobile_no',
|
||||
@ -140,23 +138,23 @@ watch(
|
||||
)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
label: 'Name',
|
||||
key: 'lead_name',
|
||||
type: 'avatar',
|
||||
size: 'w-44',
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organization_name',
|
||||
type: 'logo',
|
||||
size: 'w-44',
|
||||
size: 'w-48',
|
||||
},
|
||||
{
|
||||
label: 'Amount',
|
||||
key: 'annual_revenue',
|
||||
type: 'data',
|
||||
size: 'w-24',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'deal_status',
|
||||
type: 'indicator',
|
||||
size: 'w-44',
|
||||
size: 'w-36',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
@ -168,13 +166,19 @@ const columns = [
|
||||
label: 'Mobile no',
|
||||
key: 'mobile_no',
|
||||
type: 'phone',
|
||||
size: 'w-44',
|
||||
size: 'w-32',
|
||||
},
|
||||
{
|
||||
label: 'Lead owner',
|
||||
key: 'lead_owner',
|
||||
type: 'avatar',
|
||||
size: 'w-44',
|
||||
size: 'w-36',
|
||||
},
|
||||
{
|
||||
label: 'Last modified',
|
||||
key: 'modified',
|
||||
type: 'pretty_date',
|
||||
size: 'w-28',
|
||||
},
|
||||
]
|
||||
|
||||
@ -182,15 +186,11 @@ const rows = computed(() => {
|
||||
return leads.data?.map((lead) => {
|
||||
return {
|
||||
name: lead.name,
|
||||
lead_name: {
|
||||
label: lead.lead_name,
|
||||
image: lead.image,
|
||||
image_label: lead.first_name,
|
||||
},
|
||||
organization_name: {
|
||||
label: lead.organization_name,
|
||||
logo: lead.organization_logo,
|
||||
},
|
||||
annual_revenue: lead.annual_revenue,
|
||||
deal_status: {
|
||||
label: lead.deal_status,
|
||||
color: dealStatuses[lead.deal_status]?.color,
|
||||
@ -198,6 +198,7 @@ const rows = computed(() => {
|
||||
email: lead.email,
|
||||
mobile_no: lead.mobile_no,
|
||||
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
|
||||
modified: lead.modified,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -19,7 +19,10 @@
|
||||
</Autocomplete>
|
||||
<Dropdown :options="statusDropdownOptions(lead.data)">
|
||||
<template #default="{ open }">
|
||||
<Button :label="lead.data.status">
|
||||
<Button
|
||||
:label="lead.data.status"
|
||||
:class="leadStatuses[lead.data.status].bgColor"
|
||||
>
|
||||
<template #prefix>
|
||||
<IndicatorIcon :class="leadStatuses[lead.data.status].color" />
|
||||
</template>
|
||||
@ -31,8 +34,8 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button icon="more-horizontal" />
|
||||
<Button label="Save" variant="solid" @click="() => updateLead()" />
|
||||
<Button label="Save" variant="solid" @click="updateLead()" />
|
||||
<Button label="Convert to deal" variant="solid" @click="convertToDeal()" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<TabGroup v-slot="{ selectedIndex }" v-if="lead.data" @change="onTabChange">
|
||||
@ -96,7 +99,13 @@
|
||||
<Button class="rounded-full h-8 w-8">
|
||||
<EmailIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button icon="message-square" class="rounded-full h-8 w-8" />
|
||||
<Tooltip text="Go to website...">
|
||||
<Button
|
||||
icon="link"
|
||||
@click="openWebsite(lead.data.website)"
|
||||
class="rounded-full h-8 w-8"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button icon="more-horizontal" class="rounded-full h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
@ -158,6 +167,14 @@
|
||||
/>
|
||||
<Autocomplete
|
||||
v-else-if="field.type === 'link'"
|
||||
:value="lead.data[field.name]"
|
||||
:options="field.options"
|
||||
@change="(e) => field.change(e)"
|
||||
:placeholder="field.placeholder"
|
||||
class="form-control"
|
||||
/>
|
||||
<Autocomplete
|
||||
v-else-if="field.type === 'user'"
|
||||
:options="activeAgents"
|
||||
:value="getUser(lead.data[field.name]).full_name"
|
||||
@change="
|
||||
@ -264,7 +281,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import CommunicationArea from '@/components/CommunicationArea.vue'
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
||||
import { dateFormat, timeAgo, dateTooltipFormat, leadStatuses, statusDropdownOptions } from '@/utils'
|
||||
import {
|
||||
dateFormat,
|
||||
timeAgo,
|
||||
dateTooltipFormat,
|
||||
leadStatuses,
|
||||
statusDropdownOptions,
|
||||
openWebsite,
|
||||
} from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import {
|
||||
createResource,
|
||||
@ -277,8 +301,10 @@ import {
|
||||
Avatar,
|
||||
} from 'frappe-ui'
|
||||
import { ref, computed, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { getUser, users } = usersStore()
|
||||
const router = useRouter()
|
||||
|
||||
const makeCall = inject('makeOutgoingCall')
|
||||
|
||||
@ -345,11 +371,11 @@ const tabs = computed(() => {
|
||||
),
|
||||
activityTitle: 'Calls',
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
activityTitle: 'Tasks',
|
||||
},
|
||||
// {
|
||||
// label: 'Tasks',
|
||||
// icon: TaskIcon,
|
||||
// activityTitle: 'Tasks',
|
||||
// },
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
@ -379,17 +405,6 @@ const detailSections = computed(() => {
|
||||
label: 'About this lead',
|
||||
opened: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
name: 'status',
|
||||
options: statusDropdownOptions(lead.data),
|
||||
},
|
||||
{
|
||||
label: 'Lead owner',
|
||||
type: 'link',
|
||||
name: 'lead_owner',
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
type: 'data',
|
||||
@ -400,12 +415,66 @@ const detailSections = computed(() => {
|
||||
type: 'data',
|
||||
name: 'website',
|
||||
},
|
||||
{
|
||||
label: 'Job title',
|
||||
type: 'data',
|
||||
name: 'job_title',
|
||||
},
|
||||
{
|
||||
label: 'Source',
|
||||
type: 'link',
|
||||
name: 'source',
|
||||
placeholder: 'Select source...',
|
||||
options: [
|
||||
{ label: 'Advertisement', value: 'Advertisement' },
|
||||
{ label: 'Web', value: 'Web' },
|
||||
{ label: 'Others', value: 'Others' },
|
||||
],
|
||||
change: (data) => {
|
||||
lead.data.source = data.value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Industry',
|
||||
type: 'link',
|
||||
name: 'industry',
|
||||
placeholder: 'Select industry...',
|
||||
options: [
|
||||
{ label: 'Advertising', value: 'Advertising' },
|
||||
{ label: 'Agriculture', value: 'Agriculture' },
|
||||
{ label: 'Banking', value: 'Banking' },
|
||||
{ label: 'Others', value: 'Others' },
|
||||
],
|
||||
change: (data) => {
|
||||
lead.data.industry = data.value
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Person',
|
||||
opened: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
type: 'link',
|
||||
name: 'salutation',
|
||||
placeholder: 'Mr./Mrs./Ms.',
|
||||
options: [
|
||||
{ label: 'Dr', value: 'Dr' },
|
||||
{ label: 'Mr', value: 'Mr' },
|
||||
{ label: 'Mrs', value: 'Mrs' },
|
||||
{ label: 'Ms', value: 'Ms' },
|
||||
{ label: 'Mx', value: 'Mx' },
|
||||
{ label: 'Prof', value: 'Prof' },
|
||||
{ label: 'Master', value: 'Master' },
|
||||
{ label: 'Madam', value: 'Madam' },
|
||||
{ label: 'Miss', value: 'Miss' },
|
||||
],
|
||||
change: (data) => {
|
||||
lead.data.salutation = data.value
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'First name',
|
||||
type: 'data',
|
||||
@ -444,6 +513,13 @@ const activeAgents = computed(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function convertToDeal() {
|
||||
lead.data.status = 'Qualified'
|
||||
lead.data.is_deal = 1
|
||||
updateLead()
|
||||
router.push({ name: 'Deal', params: { dealId: lead.data.name } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -453,4 +529,8 @@ const activeAgents = computed(() => {
|
||||
border-color: transparent;
|
||||
background: white;
|
||||
}
|
||||
|
||||
:deep(.form-control button svg) {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -96,6 +96,10 @@ function getFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
function getSortBy() {
|
||||
return getOrderBy() || 'modified desc'
|
||||
}
|
||||
|
||||
const leads = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'CRM Lead',
|
||||
@ -113,7 +117,7 @@ const leads = createListResource({
|
||||
'modified',
|
||||
],
|
||||
filters: getFilter(),
|
||||
orderBy: 'modified desc',
|
||||
orderBy: getSortBy(),
|
||||
pageLength: 20,
|
||||
auto: true,
|
||||
})
|
||||
@ -122,7 +126,7 @@ watch(
|
||||
() => getOrderBy(),
|
||||
(value, old_value) => {
|
||||
if (!value && !old_value) return
|
||||
leads.orderBy = getOrderBy() || 'modified desc'
|
||||
leads.orderBy = getSortBy()
|
||||
leads.reload()
|
||||
},
|
||||
{ immediate: true }
|
||||
@ -143,37 +147,43 @@ const columns = [
|
||||
label: 'Name',
|
||||
key: 'lead_name',
|
||||
type: 'avatar',
|
||||
size: 'w-44',
|
||||
size: 'w-48',
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organization_name',
|
||||
type: 'logo',
|
||||
size: 'w-44',
|
||||
size: 'w-40',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'status',
|
||||
type: 'indicator',
|
||||
size: 'w-44',
|
||||
size: 'w-36',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
key: 'email',
|
||||
type: 'email',
|
||||
size: 'w-44',
|
||||
size: 'w-40',
|
||||
},
|
||||
{
|
||||
label: 'Mobile no',
|
||||
key: 'mobile_no',
|
||||
type: 'phone',
|
||||
size: 'w-44',
|
||||
size: 'w-32',
|
||||
},
|
||||
{
|
||||
label: 'Lead owner',
|
||||
key: 'lead_owner',
|
||||
type: 'avatar',
|
||||
size: 'w-44',
|
||||
size: 'w-36',
|
||||
},
|
||||
{
|
||||
label: 'Last modified',
|
||||
key: 'modified',
|
||||
type: 'pretty_date',
|
||||
size: 'w-28',
|
||||
},
|
||||
]
|
||||
|
||||
@ -197,6 +207,7 @@ const rows = computed(() => {
|
||||
email: lead.email,
|
||||
mobile_no: lead.mobile_no,
|
||||
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
|
||||
modified: lead.modified,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -14,25 +14,58 @@ export function timeAgo(date) {
|
||||
export const dateTooltipFormat = 'ddd, MMM D, YYYY h:mm A'
|
||||
|
||||
export const leadStatuses = {
|
||||
Open: { label: 'Open', color: '!text-gray-600' },
|
||||
Contacted: { label: 'Contacted', color: '!text-orange-600' },
|
||||
Nurture: { label: 'Nurture', color: '!text-blue-600' },
|
||||
Qualified: { label: 'Qualified', color: '!text-green-600' },
|
||||
Unqualified: { label: 'Unqualified', color: '!text-red-600' },
|
||||
Junk: { label: 'Junk', color: '!text-purple-600' },
|
||||
Open: { label: 'Open', color: '!text-gray-600', bgColor: '!bg-gray-200' },
|
||||
Contacted: {
|
||||
label: 'Contacted',
|
||||
color: '!text-orange-600',
|
||||
bgColor: '!bg-orange-200',
|
||||
},
|
||||
Nurture: {
|
||||
label: 'Nurture',
|
||||
color: '!text-blue-600',
|
||||
bgColor: '!bg-blue-200',
|
||||
},
|
||||
Qualified: {
|
||||
label: 'Qualified',
|
||||
color: '!text-green-600',
|
||||
bgColor: '!bg-green-200',
|
||||
},
|
||||
Unqualified: {
|
||||
label: 'Unqualified',
|
||||
color: '!text-red-600',
|
||||
bgColor: '!bg-red-200',
|
||||
},
|
||||
Junk: { label: 'Junk', color: '!text-purple-600', bgColor: '!bg-purple-200' },
|
||||
}
|
||||
|
||||
export const dealStatuses = {
|
||||
Qualification: { label: 'Qualification', color: '!text-gray-600' },
|
||||
'Demo/Making': { label: 'Demo/Making', color: '!text-orange-600' },
|
||||
Qualification: {
|
||||
label: 'Qualification',
|
||||
color: '!text-gray-600',
|
||||
bgColor: '!bg-gray-200',
|
||||
},
|
||||
'Demo/Making': {
|
||||
label: 'Demo/Making',
|
||||
color: '!text-orange-600',
|
||||
bgColor: '!bg-orange-200',
|
||||
},
|
||||
'Proposal/Quotation': {
|
||||
label: 'Proposal/Quotation',
|
||||
color: '!text-blue-600',
|
||||
bgColor: '!bg-blue-200',
|
||||
},
|
||||
Negotiation: { label: 'Negotiation', color: '!text-yellow-600' },
|
||||
'Ready to Close': { label: 'Ready to Close', color: '!text-purple-600' },
|
||||
Won: { label: 'Won', color: '!text-green-600' },
|
||||
Lost: { label: 'Lost', color: '!text-red-600' },
|
||||
Negotiation: {
|
||||
label: 'Negotiation',
|
||||
color: '!text-yellow-600',
|
||||
bgColor: '!bg-yellow-100',
|
||||
},
|
||||
'Ready to Close': {
|
||||
label: 'Ready to Close',
|
||||
color: '!text-purple-600',
|
||||
bgColor: '!bg-purple-200',
|
||||
},
|
||||
Won: { label: 'Won', color: '!text-green-600', bgColor: '!bg-green-200' },
|
||||
Lost: { label: 'Lost', color: '!text-red-600', bgColor: '!bg-red-200' },
|
||||
}
|
||||
|
||||
export function statusDropdownOptions(data, doctype) {
|
||||
@ -56,3 +89,7 @@ export function statusDropdownOptions(data, doctype) {
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
export function openWebsite(url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user