eat(approvals): add and define components and other files related to timesheet approval page
This commit is contained in:
parent
c1ce7e36cb
commit
1f94d6a900
BIN
src/assets/targo-default-avatar.png
Normal file
BIN
src/assets/targo-default-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
0
src/modules/employee-list/composables/use-users.ts
Normal file
0
src/modules/employee-list/composables/use-users.ts
Normal file
0
src/modules/employee-list/employee-constants.ts
Normal file
0
src/modules/employee-list/employee-constants.ts
Normal file
0
src/modules/employee-list/employee-store.ts
Normal file
0
src/modules/employee-list/employee-store.ts
Normal file
0
src/modules/employee-list/pages/user-add-page.vue
Normal file
0
src/modules/employee-list/pages/user-add-page.vue
Normal file
0
src/modules/employee-list/services/user-service.ts
Normal file
0
src/modules/employee-list/services/user-service.ts
Normal file
12
src/modules/shared/components/navigation/footer-bar.vue
Normal file
12
src/modules/shared/components/navigation/footer-bar.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LanguageSwitch from '../language-switch.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-footer elevated class="bg-primary text-white">
|
||||||
|
<q-toolbar>
|
||||||
|
<q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title>
|
||||||
|
<LanguageSwitch class="q-mr-xs text-white" />
|
||||||
|
</q-toolbar>
|
||||||
|
</q-footer>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
const currentUser = authStore.user;
|
||||||
|
|
||||||
|
// Will need to implement this eventually, just testing the look for now
|
||||||
|
const notifAmount = ref(7);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-item clickable v-ripple dark @click="uiStore.toggleRightDrawer">
|
||||||
|
<q-item-section side>
|
||||||
|
<q-avatar rounded >
|
||||||
|
<q-img src="src/assets/targo-default-avatar.png" />
|
||||||
|
<q-badge floating color="red" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ currentUser.firstName }} {{ currentUser.lastName }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ notifAmount }} new messages</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
22
src/modules/shared/components/navigation/header-bar.vue
Normal file
22
src/modules/shared/components/navigation/header-bar.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import HeaderBarAvatar from './header-bar-avatar.vue';
|
||||||
|
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-header elevated>
|
||||||
|
<q-toolbar>
|
||||||
|
<q-toolbar-title>
|
||||||
|
<q-btn flat dense color="white" icon="menu" @click="uiStore.toggleRightDrawer">
|
||||||
|
<q-img src="src/assets/logo-targo-white.svg" fit="contain" width="150px" height="30px"/>
|
||||||
|
</q-btn>
|
||||||
|
</q-toolbar-title>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<HeaderBarAvatar />
|
||||||
|
</q-item>
|
||||||
|
</q-toolbar>
|
||||||
|
</q-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
88
src/modules/shared/components/navigation/right-drawer.vue
Normal file
88
src/modules/shared/components/navigation/right-drawer.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { hasRequiredRole } from 'src/utils/has-required-role';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const miniState = ref(true);
|
||||||
|
|
||||||
|
const goToPageName = (pageName: string) => {
|
||||||
|
router.push({ name: pageName }).catch(err => {
|
||||||
|
console.error('Error with Vue Router: ', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
authStore.logout();
|
||||||
|
|
||||||
|
router.push({ name: 'login' }).catch(err => {
|
||||||
|
console.log('could not log you out: ', err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-drawer overlay elevated side="left" :mini="miniState" @mouseenter="miniState = false"
|
||||||
|
@mouseleave="miniState = true" v-model="uiStore.isRightDrawerOpen">
|
||||||
|
<q-scroll-area class="fit">
|
||||||
|
<q-list>
|
||||||
|
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
||||||
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||||
|
v-if="hasRequiredRole('supervisor', 'accounting')">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="event_available" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ $t('navBar.userMenuShiftValidation') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
||||||
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||||
|
v-if="hasRequiredRole('supervisor', 'human resources', 'accounting')">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="view_list" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ $t('navBar.userMenuEmployeeList') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<!-- Profile -->
|
||||||
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.PROFILE)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="account_box" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ $t('navBar.userMenuProfile') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<!-- Help -->
|
||||||
|
<q-item v-ripple clickable @click="goToPageName('help')">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="contact_support" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ $t('navBar.userMenuHelp') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<!-- Logout -->
|
||||||
|
<q-item v-ripple clickable @click="handleLogout" class="absolute-bottom">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="exit_to_app" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ $t('navBar.userMenuLogout') }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-scroll-area>
|
||||||
|
</q-drawer>
|
||||||
|
</template>
|
||||||
7
src/modules/shared/types/pay-period-interface.ts
Normal file
7
src/modules/shared/types/pay-period-interface.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface PayPeriod {
|
||||||
|
period_number: number;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
year: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
6
src/modules/shared/types/user-interface.ts
Normal file
6
src/modules/shared/types/user-interface.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface User {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
|
const timesheetStore = useTimesheetStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<q-btn-group outline rounded>
|
||||||
|
<q-btn outline icon="arrow_circle_left"/>
|
||||||
|
<q-btn outline icon="calendar_month" :label="timesheetStore.currentPayPeriod?.label"/>
|
||||||
|
<q-btn outline icon="arrow_circle_right"/>
|
||||||
|
</q-btn-group>
|
||||||
|
<div class="text-primary text-h4">{{ timesheetStore.currentPayPeriod?.label }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const testDates: string[] = ['25 Juin 2025', '12 Juillet 2025', '13 Juillet 2025', '26 Juillet 2025', '27 Juillet 2025', '9 Aout 2025']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page padding class="q-pa-md bg-secondary">
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { api } from "src/boot/axios";
|
||||||
|
import { mock_pay_period_employee_overviews, mock_pay_periods } from "../timesheet-approval-test-constants";
|
||||||
|
import { PayPeriod } from "src/modules/shared/types/pay-period-interface";
|
||||||
|
|
||||||
|
export const timesheetApprovalService = {
|
||||||
|
getCurrentPayPeriod: async () :Promise<PayPeriod> => {
|
||||||
|
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
|
||||||
|
return await api.get(`/pay-periods/date/${new Date()}`) || mock_pay_periods[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllPayPeriods: async () => {
|
||||||
|
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
|
||||||
|
return await api.get(`/pay-periods/`) || mock_pay_periods;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPayPeriodEmployeeOverviews: async (period_number: number) => {
|
||||||
|
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
|
||||||
|
return await api.get(`/pay-periods/${period_number}/overview`) || mock_pay_period_employee_overviews;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { PayPeriod } from "../shared/types/pay-period-interface";
|
||||||
|
import { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface"
|
||||||
|
|
||||||
|
export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
|
||||||
|
{
|
||||||
|
"employee_id": 'EMP-001',
|
||||||
|
"employee_name": 'Alice Johnson',
|
||||||
|
"regular_hours": 75,
|
||||||
|
"evening_hours": 12,
|
||||||
|
"emergency_hours": 3,
|
||||||
|
"overtime_hours": 5,
|
||||||
|
"expenses": 120.50,
|
||||||
|
"mileage": 45,
|
||||||
|
"is_approved": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"employee_id": 'EMP-002',
|
||||||
|
"employee_name": 'Brian Smith',
|
||||||
|
"regular_hours": 80,
|
||||||
|
"evening_hours": 8,
|
||||||
|
"emergency_hours": 0,
|
||||||
|
"overtime_hours": 2,
|
||||||
|
"expenses": 75.00,
|
||||||
|
"mileage": 12,
|
||||||
|
"is_approved": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"employee_id": 'EMP-003',
|
||||||
|
"employee_name": 'Chloe Ramirez',
|
||||||
|
"regular_hours": 68,
|
||||||
|
"evening_hours": 15,
|
||||||
|
"emergency_hours": 1,
|
||||||
|
"overtime_hours": 0,
|
||||||
|
"expenses": 200.00,
|
||||||
|
"mileage": 88,
|
||||||
|
"is_approved": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"employee_id": 'EMP-004',
|
||||||
|
"employee_name": 'David Lee',
|
||||||
|
"regular_hours": 82,
|
||||||
|
"evening_hours": 5,
|
||||||
|
"emergency_hours": 4,
|
||||||
|
"overtime_hours": 6,
|
||||||
|
"expenses": 50.75,
|
||||||
|
"mileage": 20,
|
||||||
|
"is_approved": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"employee_id": 'EMP-005',
|
||||||
|
"employee_name": 'Emily Carter',
|
||||||
|
"regular_hours": 78,
|
||||||
|
"evening_hours": 10,
|
||||||
|
"emergency_hours": 2,
|
||||||
|
"overtime_hours": 3,
|
||||||
|
"expenses": 95.25,
|
||||||
|
"mileage": 60,
|
||||||
|
"is_approved": false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mock_pay_periods: PayPeriod[] = [
|
||||||
|
{
|
||||||
|
"period_number": 15,
|
||||||
|
"start_date": "2025-07-27",
|
||||||
|
"end_date": "2025-08-09",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-07-27 → 2025-08-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 14,
|
||||||
|
"start_date": "2025-07-13",
|
||||||
|
"end_date": "2025-07-26",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-07-13 → 2025-07-26"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 13,
|
||||||
|
"start_date": "2025-06-29",
|
||||||
|
"end_date": "2025-07-12",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-06-29 → 2025-07-12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 12,
|
||||||
|
"start_date": "2025-06-15",
|
||||||
|
"end_date": "2025-06-28",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-06-15 → 2025-06-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 11,
|
||||||
|
"start_date": "2025-06-01",
|
||||||
|
"end_date": "2025-06-14",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-06-01 → 2025-06-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 10,
|
||||||
|
"start_date": "2025-05-18",
|
||||||
|
"end_date": "2025-05-31",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-05-18 → 2025-05-31"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 9,
|
||||||
|
"start_date": "2025-05-04",
|
||||||
|
"end_date": "2025-05-17",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-05-04 → 2025-05-17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 8,
|
||||||
|
"start_date": "2025-04-20",
|
||||||
|
"end_date": "2025-05-03",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-04-20 → 2025-05-03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 7,
|
||||||
|
"start_date": "2025-04-06",
|
||||||
|
"end_date": "2025-04-19",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-04-06 → 2025-04-19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"period_number": 6,
|
||||||
|
"start_date": "2025-03-23",
|
||||||
|
"end_date": "2025-04-05",
|
||||||
|
"year": 2025,
|
||||||
|
"label": "2025-03-23 → 2025-04-05"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface PayPeriodEmployeeOverview {
|
||||||
|
employee_id: string;
|
||||||
|
employee_name: string;
|
||||||
|
regular_hours: number;
|
||||||
|
evening_hours: number;
|
||||||
|
emergency_hours: number;
|
||||||
|
overtime_hours: number;
|
||||||
|
expenses: number;
|
||||||
|
mileage: number;
|
||||||
|
is_approved: boolean;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface PayPeriodOverview {
|
||||||
|
period_number: number;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
9
src/router/router-constants.ts
Normal file
9
src/router/router-constants.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export enum RouteNames {
|
||||||
|
/* eslint-disable */
|
||||||
|
LOGIN = 'login',
|
||||||
|
LOGIN_SUCCESS = 'login-success',
|
||||||
|
DASHBOARD = 'dashboard',
|
||||||
|
TIMESHEET_APPROVALS = 'timesheet-approvals',
|
||||||
|
EMPLOYEE_LIST = 'employee-list',
|
||||||
|
PROFILE = 'user/profile',
|
||||||
|
}
|
||||||
39
src/stores/auth-store.ts
Normal file
39
src/stores/auth-store.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { AuthService } from "../modules/auth/services/services-auth";
|
||||||
|
import type { User } from "src/modules/shared/types/user-interface";
|
||||||
|
|
||||||
|
const defaultUser: User = {
|
||||||
|
firstName: 'Unknown',
|
||||||
|
lastName: 'Unknown',
|
||||||
|
email: 'guest@guest.com',
|
||||||
|
role: 'guest'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const user = ref (defaultUser);
|
||||||
|
const authError = ref("");
|
||||||
|
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
//TODO: manage customer login process
|
||||||
|
};
|
||||||
|
|
||||||
|
const oidcLogin = () => {
|
||||||
|
const oidcPopup = AuthService.oidcLogin();
|
||||||
|
if (!oidcPopup) {
|
||||||
|
authError.value = "You have popups blocked on this website!";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
user.value = defaultUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUser = (currentUser: User) => {
|
||||||
|
user.value = currentUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
|
||||||
|
});
|
||||||
|
|
||||||
18
src/stores/timesheet-store.ts
Normal file
18
src/stores/timesheet-store.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
||||||
|
|
||||||
|
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
|
const payPeriods = ref<PayPeriod[]>([]);
|
||||||
|
const currentPayPeriod = ref<PayPeriod>();
|
||||||
|
|
||||||
|
const getCurrentPayPeriod = async () => {
|
||||||
|
if (!currentPayPeriod.value) {
|
||||||
|
currentPayPeriod.value = await timesheetApprovalService.getCurrentPayPeriod();
|
||||||
|
}
|
||||||
|
return currentPayPeriod.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { payPeriods, currentPayPeriod, getCurrentPayPeriod};
|
||||||
|
});
|
||||||
11
src/stores/ui-store.ts
Normal file
11
src/stores/ui-store.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const useUiStore = defineStore('ui', () => {
|
||||||
|
const isRightDrawerOpen = ref(true);
|
||||||
|
const toggleRightDrawer = () => {
|
||||||
|
isRightDrawerOpen.value = !isRightDrawerOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isRightDrawerOpen, toggleRightDrawer };
|
||||||
|
});
|
||||||
7
src/utils/has-required-role.ts
Normal file
7
src/utils/has-required-role.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
|
|
||||||
|
export const hasRequiredRole = (...requiredRoles: string[] ) => {
|
||||||
|
const currentUserRole = useAuthStore().user.role;
|
||||||
|
|
||||||
|
return requiredRoles.includes(currentUserRole);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user