Warranty polish: hero badges everywhere, search/pagination, re-check, shared apiError
- Shared utils/apiError.js reads the correct nested error message (with fallbacks); swept 37 views/components off the shallow path so real backend messages (e.g. in-use 409s) surface instead of generic "Failed". - useWarrantyBadge composable: warranty hero status/end-date badge now on PC, equipment, printer, and network detail heroes (one shared fetch feeds the badge + the WarrantyPanel). - Warranties list: client-side search (vendor/level/tag/asset) + pagination; truncate long service levels so they stop blowing out the table width. - "Re-check all" button + POST /warranty/sync/dell?all=true to re-pull dated Dell warranties, not just missing ones. - Deprecate the standalone pxe-images/warranty_sync.py in favor of the plugin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { computersApi } from '@/api'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const protocols = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -151,7 +152,7 @@ async function save() {
|
||||
editing.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Save failed.'
|
||||
error.value = apiError(err, 'Save failed.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { assetsApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -106,7 +107,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { businessunitsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -168,7 +169,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { assetsApi, customFieldsApi } from '../../api'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const assetTypes = ref([])
|
||||
@@ -196,7 +197,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadFields()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -208,7 +209,7 @@ async function deleteField(f) {
|
||||
await customFieldsApi.remove(f.fieldid)
|
||||
loadFields()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { dashboardDefaultsApi, businessUnitsApi } from '../../api'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -168,7 +169,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import { equipmentApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -126,7 +127,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
||||
await equipmentApi.types.remove(t.equipmenttypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,6 +77,7 @@ import { locationsApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -126,7 +127,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
||||
await locationsApi.types.remove(t.locationtypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -212,6 +212,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const locations = ref([])
|
||||
@@ -354,7 +355,7 @@ async function saveLocation() {
|
||||
loadLocations()
|
||||
} catch (err) {
|
||||
console.error('Error saving location:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save location'
|
||||
error.value = apiError(err, 'Failed to save location')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { machinetypesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const machineTypes = ref([])
|
||||
@@ -239,7 +240,7 @@ async function saveType() {
|
||||
loadTypes()
|
||||
} catch (err) {
|
||||
console.error('Error saving machine type:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save machine type'
|
||||
error.value = apiError(err, 'Failed to save machine type')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { printersApi } from '@/api'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const models = ref([])
|
||||
const loadingModels = ref(true)
|
||||
@@ -246,7 +247,7 @@ async function save() {
|
||||
await selectModel(selectedModel.value)
|
||||
await loadModels()
|
||||
} catch (err) {
|
||||
formError.value = err.response?.data?.error?.message || 'Save failed.'
|
||||
formError.value = apiError(err, 'Save failed.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const models = ref([])
|
||||
@@ -328,7 +329,7 @@ async function saveModel() {
|
||||
loadModels()
|
||||
} catch (err) {
|
||||
console.error('Error saving model:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save model'
|
||||
error.value = apiError(err, 'Failed to save model')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import { networkApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -126,7 +127,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
||||
await networkApi.types.remove(t.networkdevicetypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -120,6 +120,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { operatingsystemsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -193,7 +194,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ import { computersApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const pcTypes = ref([])
|
||||
@@ -132,7 +133,7 @@ async function deleteType(pt) {
|
||||
await computersApi.types.remove(pt.computertypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { printersApi } from '../../api'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -161,7 +162,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -173,7 +174,7 @@ async function deleteDriver(d) {
|
||||
await printersApi.drivers.delete(d.driverid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,6 +77,7 @@ import { printersApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -126,7 +127,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
||||
await printersApi.types.remove(t.printertypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,6 +77,7 @@ import { relationshipTypesApi } from '../../api'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
@@ -126,7 +127,7 @@ async function save() {
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
||||
error.value = apiError(err, 'Failed to save')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
||||
await relationshipTypesApi.remove(t.relationshiptypeid)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
||||
toast.error(apiError(err, 'Failed to delete'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { settingsApi } from '@/api'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -71,7 +72,7 @@ async function save() {
|
||||
}
|
||||
saved.value = true
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error?.message || 'Save failed.'
|
||||
error.value = apiError(err, 'Save failed.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ import { assetsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const statuses = ref([])
|
||||
@@ -233,7 +234,7 @@ async function saveStatus() {
|
||||
loadStatuses()
|
||||
} catch (err) {
|
||||
console.error('Error saving status:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save status'
|
||||
error.value = apiError(err, 'Failed to save status')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -252,7 +253,7 @@ async function deleteStatus() {
|
||||
loadStatuses()
|
||||
} catch (err) {
|
||||
console.error('Error deleting status:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to delete status'
|
||||
error.value = apiError(err, 'Failed to delete status')
|
||||
toast.error(error.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,7 @@ import { useRoute } from 'vue-router'
|
||||
import { networkApi, locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const route = useRoute()
|
||||
@@ -472,7 +473,7 @@ async function saveSubnet() {
|
||||
loadSubnets()
|
||||
} catch (err) {
|
||||
console.error('Error saving subnet:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save subnet'
|
||||
error.value = apiError(err, 'Failed to save subnet')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -491,7 +492,7 @@ async function deleteSubnet() {
|
||||
loadSubnets()
|
||||
} catch (err) {
|
||||
console.error('Error deleting subnet:', err)
|
||||
toast.error(err.response?.data?.message || 'Failed to delete subnet')
|
||||
toast.error(apiError(err, 'Failed to delete subnet'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -657,6 +657,7 @@ import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { settingsApi, computersApi } from '../../api'
|
||||
import { setIdentifierFlag } from '../../composables/identifierSettings'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
// Section tabs - one section visible at a time to avoid a long scroll. The
|
||||
// search box filters tabs (and, while searching, shows every matching section).
|
||||
@@ -900,7 +901,7 @@ async function changePcTypeMapping(pxetype, computertype) {
|
||||
success.value = 'Setting saved'
|
||||
setTimeout(() => { success.value = '' }, 2000)
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
||||
error.value = apiError(e, 'Failed to save setting')
|
||||
console.error(e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
@@ -943,7 +944,7 @@ async function toggleIdentifier(name, assettype) {
|
||||
success.value = 'Setting saved'
|
||||
setTimeout(() => { success.value = '' }, 2000)
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
||||
error.value = apiError(e, 'Failed to save setting')
|
||||
console.error(e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
@@ -977,7 +978,7 @@ async function toggleSearchDomain(domainKey) {
|
||||
success.value = 'Setting saved'
|
||||
setTimeout(() => { success.value = '' }, 2000)
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
||||
error.value = apiError(e, 'Failed to save setting')
|
||||
console.error(e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
@@ -996,7 +997,7 @@ async function saveSetting(key, value) {
|
||||
success.value = 'Setting saved'
|
||||
setTimeout(() => { success.value = '' }, 2000)
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
||||
error.value = apiError(e, 'Failed to save setting')
|
||||
console.error(e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
@@ -1013,7 +1014,7 @@ async function testEmail() {
|
||||
// await settingsApi.testEmail()
|
||||
success.value = 'Test email feature coming soon'
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || 'Failed to send test email'
|
||||
error.value = apiError(e, 'Failed to send test email')
|
||||
} finally {
|
||||
testingEmail.value = false
|
||||
setTimeout(() => { success.value = '' }, 3000)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -188,6 +188,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { networkApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const vlans = ref([])
|
||||
@@ -309,7 +310,7 @@ async function saveVLAN() {
|
||||
loadVLANs()
|
||||
} catch (err) {
|
||||
console.error('Error saving VLAN:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save VLAN'
|
||||
error.value = apiError(err, 'Failed to save VLAN')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -328,7 +329,7 @@ async function deleteVLAN() {
|
||||
loadVLANs()
|
||||
} catch (err) {
|
||||
console.error('Error deleting VLAN:', err)
|
||||
toast.error(err.response?.data?.message || 'Failed to delete VLAN')
|
||||
toast.error(apiError(err, 'Failed to delete VLAN'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user