feat(timesheet): added navigation and save btns
This commit is contained in:
parent
42219171a9
commit
29f5760c62
|
|
@ -0,0 +1,104 @@
|
|||
<script setup lang="ts">
|
||||
/* eslint-disable */
|
||||
import { ref } from 'vue';
|
||||
import { date } from 'quasar';
|
||||
import type { QDateDetails } from 'src/modules/shared/types/q-date-details';
|
||||
|
||||
const is_showing_calendar_picker = ref(false);
|
||||
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
|
||||
|
||||
const props = defineProps<{
|
||||
isDisabled: boolean,
|
||||
isPreviousLimit: boolean,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'date-selected': [value: string, reason?: string, details?: QDateDetails]
|
||||
'pressed-previous-button': []
|
||||
'pressed-next-button': []
|
||||
'pressed-current-button' : []
|
||||
}>();
|
||||
|
||||
const onDateSelected = (value: string, reason: string, details: QDateDetails) => {
|
||||
calendar_date.value = value;
|
||||
is_showing_calendar_picker.value = false;
|
||||
emit('date-selected', value, reason, details);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-mb-lg q-mt-lg col-12" >
|
||||
<q-btn
|
||||
push rounded
|
||||
icon="keyboard_arrow_left"
|
||||
color="primary"
|
||||
@click="emit('pressed-previous-button')"
|
||||
:disable="props.isPreviousLimit || props.isDisabled"
|
||||
class="q-mr-sm q-px-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
class="bg-primary text-uppercase text-weight-bold"
|
||||
> Semaine précédante
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
push rounded
|
||||
icon="today"
|
||||
color="primary"
|
||||
@click="emit('pressed-current-button')"
|
||||
:disable="props.isDisabled"
|
||||
class="q-mr-sm q-px-lg"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
class="bg-primary text-uppercase text-weight-bold"
|
||||
>Cette Semaine
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
push rounded
|
||||
icon="calendar_month"
|
||||
color="primary"
|
||||
@click="is_showing_calendar_picker = true"
|
||||
:disable="props.isDisabled"
|
||||
class="q-px-lg"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
class="bg-primary text-uppercase text-weight-bold"
|
||||
> calendrier
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
push rounded
|
||||
icon="keyboard_arrow_right"
|
||||
color="primary"
|
||||
@click="emit('pressed-next-button')"
|
||||
:disable="props.isDisabled"
|
||||
class="q-ml-sm q-px-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
class="bg-primary text-uppercase text-weight-bold"
|
||||
> Semaine suivante
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="is_showing_calendar_picker" transition-show="jump-down" transition-hide="jump-up" position="top">
|
||||
<q-date
|
||||
v-model="calendar_date"
|
||||
color="primary"
|
||||
class="q-mt-xl"
|
||||
today-btn
|
||||
mask="YYYY-MM-DD"
|
||||
:options="date => date > '2023-12-16'"
|
||||
@update:model-value="onDateSelected"
|
||||
/>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -2,8 +2,13 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable */
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import TimesheetShiftComment from '../shift/timesheet-shift-comment.vue';
|
||||
import TimesheetNavigation from './timesheet-navigation.vue';
|
||||
import type { CreateShiftPayload } from '../../types/timesheet-shift-interface';
|
||||
import { useTimesheetApi } from '../../composables/use-timesheet-api';
|
||||
import { date as qdate } from 'quasar';
|
||||
|
||||
type ShiftRow = {
|
||||
type: string;
|
||||
|
|
@ -13,31 +18,125 @@ type ShiftRow = {
|
|||
is_approved: boolean;
|
||||
}
|
||||
|
||||
//plug via findAll bank_codes
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', payload: CreateShiftPayload[]): void;
|
||||
}>();
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
const shift_type = ref<string[]> (['Régulier','Télétravail', 'Soir', 'Urgence', 'Férier', 'Vacance', 'Maladie']);
|
||||
|
||||
|
||||
const days = ['Dimanche', 'Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'];
|
||||
const default_rows = days.map(index => ({ index, type: '', start_time: '', end_time: '', comment: '', is_approved: false, }));
|
||||
const empty_row = { type: '', start_time: '', end_time: '', comment: '', is_approved: false };
|
||||
|
||||
const rows = ref<ShiftRow[]>(default_rows);
|
||||
const empty_row = {
|
||||
type: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
comment: '',
|
||||
is_approved: false,
|
||||
}
|
||||
//Week dates
|
||||
const week_dates = computed(() => {
|
||||
const start_date = timesheet_store.current_timesheet.start_day;
|
||||
if(!start_date) return [];
|
||||
|
||||
const mm = /^(\d{4})-(\d{2})-(\d{2})$/.exec(start_date);
|
||||
if(!mm) return [];
|
||||
const year = Number(mm[1]), month = Number(mm[2]), day = Number(mm[3])
|
||||
|
||||
const base = new Date(year, month - 1, day);
|
||||
const yyyymmdd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
|
||||
return Array.from({length:7 }, (_, i) => {
|
||||
const date = new Date(base);
|
||||
date.setDate(base.getDate() + i);
|
||||
return yyyymmdd(date);
|
||||
});
|
||||
});
|
||||
|
||||
//filling timesheet with shifts
|
||||
const rows = ref<ShiftRow[]>(
|
||||
days.map((_,index) => {
|
||||
const date_ISO = week_dates.value[index];
|
||||
const shift = timesheet_store.current_timesheet.shifts.find(sh => sh.date === date_ISO);
|
||||
return shift ? {
|
||||
type: shift.bank_type || '',
|
||||
start_time: shift.start_time || '',
|
||||
end_time: shift.end_time || '',
|
||||
comment: shift.description || '',
|
||||
is_approved: !!shift.is_approved,
|
||||
}
|
||||
: { ...empty_row };
|
||||
})
|
||||
);
|
||||
|
||||
const selected_row = ref<ShiftRow>(empty_row);
|
||||
const show_comment = ref(false);
|
||||
const onClickComment = (row: ShiftRow)=> {
|
||||
selected_row.value = row;
|
||||
show_comment.value = true;
|
||||
const selected_index = ref<number | null>(null);
|
||||
const selected_row = computed<ShiftRow | undefined>(()=>
|
||||
selected_index.value != null ? rows.value[selected_index.value] : undefined
|
||||
);
|
||||
|
||||
const is_calendar_limit = computed( () => {
|
||||
return timesheet_store.current_pay_period.pay_year === 2024 &&
|
||||
timesheet_store.current_pay_period.pay_period_no <= 1;
|
||||
});
|
||||
|
||||
const onDateSelected = async (date_string: string) => {
|
||||
const when = qdate.extractDate(date_string, 'YYYY-MM-DD');
|
||||
timesheet_api.getCurrentWeekTimesheetOverview(when);
|
||||
};
|
||||
|
||||
const setComment = (comment: string) => {
|
||||
if(selected_row.value) {
|
||||
selected_row.value.comment = comment;
|
||||
}
|
||||
show_comment.value = false;
|
||||
}
|
||||
|
||||
const onClickComment = (index: number)=> {
|
||||
selected_index.value = index;
|
||||
show_comment.value = true;
|
||||
};
|
||||
|
||||
const buildPayload = (): CreateShiftPayload[] => {
|
||||
const dates = week_dates.value;
|
||||
if(!Array.isArray(dates) || dates.length === 0) return [];
|
||||
|
||||
return rows.value.flatMap((row, idx) => {
|
||||
const date = dates[idx];
|
||||
const has_data = !!(row.type || row.start_time || row.end_time || row.comment);
|
||||
if(!date || !has_data) return [];
|
||||
|
||||
const item: CreateShiftPayload = {
|
||||
date,
|
||||
type: row.type,
|
||||
start_time: row.start_time,
|
||||
end_time: row.end_time,
|
||||
};
|
||||
if(row.comment) item.description = row.comment;
|
||||
return[item];
|
||||
})
|
||||
};
|
||||
|
||||
const saveWeek = () => {
|
||||
const payload = buildPayload();
|
||||
emit('save', payload);
|
||||
}
|
||||
|
||||
const clearRow = (idx: number) => {
|
||||
rows.value[idx] = { ...empty_row };
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [timesheet_store.current_timesheet.start_day, timesheet_store.current_timesheet.shifts],
|
||||
() => {
|
||||
const dates = week_dates.value;
|
||||
rows.value = days.map((_, idx)=> {
|
||||
const shift = timesheet_store.current_timesheet.shifts.find(sh => sh.date === dates[idx]);
|
||||
return shift
|
||||
? { type: shift.bank_type || '', start_time: shift.start_time || '', end_time: shift.end_time || '', comment: shift.description || '', is_approved: shift.is_approved}
|
||||
: { type: '', start_time: '', end_time: '', comment: '', is_approved: false };
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template >
|
||||
<q-card class="q-pa-md q-ma-md">
|
||||
<q-dialog
|
||||
v-model="show_comment"
|
||||
|
|
@ -46,19 +145,27 @@ const onClickComment = (row: ShiftRow)=> {
|
|||
persistent
|
||||
>
|
||||
<TimesheetShiftComment
|
||||
:comment-string="selected_row.comment"
|
||||
@click-save="comment => selected_row.comment = comment"
|
||||
:comment-string="selected_row?.comment ?? ''"
|
||||
@click-save="setComment"
|
||||
@click-close="show_comment = false"
|
||||
/>
|
||||
</q-dialog>
|
||||
<q-form autofocus >
|
||||
<div v-for="row, index in rows" :key="index" class="row q-gutter-sm">
|
||||
<span class="text-weight-medium col">{{ days[index] }}</span>
|
||||
<q-form autofocus class="q-ml-xl">
|
||||
<TimesheetNavigation
|
||||
:is-disabled="timesheet_store.is_loading"
|
||||
:is-previous-limit="is_calendar_limit"
|
||||
@date-selected="onDateSelected"
|
||||
@pressed-previous-button="timesheet_api.previous_week()"
|
||||
@pressed-current-button="timesheet_api.getCurrentWeekTimesheetOverview()"
|
||||
@pressed-next-button="timesheet_api.next_week()"
|
||||
/>
|
||||
<div v-for="row, index in rows" :key="index" class="row q-gutter-sm q-mb-sm">
|
||||
<span class="text-weight-bold text-primary col-1">{{ days[index] }}</span>
|
||||
<q-select
|
||||
v-model="row.type"
|
||||
:options="shift_type"
|
||||
:readonly="row.is_approved"
|
||||
class="col"
|
||||
class="col-2"
|
||||
label="Type de Quart"
|
||||
dense
|
||||
filled
|
||||
|
|
@ -67,21 +174,11 @@ const onClickComment = (row: ShiftRow)=> {
|
|||
options-dense
|
||||
map-options
|
||||
hide-dropdown-icon
|
||||
>
|
||||
<template v-slot:before>
|
||||
<q-btn
|
||||
icon="cleaning_services"
|
||||
flat
|
||||
dense
|
||||
class="q-pa-none q-ma-none"
|
||||
@click="rows[index] = empty_row"
|
||||
/>
|
||||
</template>
|
||||
</q-select>
|
||||
/>
|
||||
<q-input
|
||||
v-model="row.start_time"
|
||||
:readonly="row.is_approved"
|
||||
class="col"
|
||||
class="col-2"
|
||||
label="Entrée"
|
||||
dense
|
||||
filled
|
||||
|
|
@ -93,7 +190,7 @@ const onClickComment = (row: ShiftRow)=> {
|
|||
<q-input
|
||||
v-model="row.end_time"
|
||||
:readonly="row.is_approved"
|
||||
class="col"
|
||||
class="col-2"
|
||||
label="Sortie"
|
||||
dense
|
||||
filled
|
||||
|
|
@ -108,9 +205,35 @@ const onClickComment = (row: ShiftRow)=> {
|
|||
:color="row.comment.length > 0 ? 'primary' : 'grey-8'"
|
||||
:disable="row.is_approved"
|
||||
flat
|
||||
@click="onClickComment(row)"
|
||||
class="col-1"
|
||||
@click="onClickComment(index)"
|
||||
/>
|
||||
<q-btn
|
||||
icon="cleaning_services"
|
||||
flat
|
||||
dense
|
||||
class="q-pa-none q-ma-none justify-right"
|
||||
:color="row.type.length || row.start_time.length || row.end_time.length || row.comment.length > 0 ? 'primary' : 'accent'"
|
||||
@click="clearRow(index)"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save_alt"
|
||||
class="col-1"
|
||||
rounded
|
||||
push
|
||||
@click="saveWeek"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="center middle"
|
||||
class="bg-primary text-uppercase text-weight-bold"
|
||||
>Sauvegarder
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
|
@ -2,6 +2,7 @@ import { useAuthStore } from "src/stores/auth-store";
|
|||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
||||
import { ref } from "vue";
|
||||
import { timesheetTempService } from "../services/timesheet-services";
|
||||
import type { CreateShiftPayload } from "../types/timesheet-shift-interface";
|
||||
|
||||
|
||||
export const useTimesheetApi = () => {
|
||||
|
|
@ -9,7 +10,7 @@ export const useTimesheetApi = () => {
|
|||
const auth_store = useAuthStore();
|
||||
const week_offset = ref(0);
|
||||
|
||||
const fetch_week = async (offset = week_offset.value) => {
|
||||
const fetchWeek = async (offset = week_offset.value) => {
|
||||
const email = auth_store.user?.email;
|
||||
if(!email) return;
|
||||
try{
|
||||
|
|
@ -25,9 +26,35 @@ export const useTimesheetApi = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const this_week = async () => fetch_week(0);
|
||||
const next_week = async () => fetch_week(week_offset.value + 1);
|
||||
const previous_week = async () => fetch_week(week_offset.value - 1);
|
||||
const this_week = async () => fetchWeek(0);
|
||||
const next_week = async () => fetchWeek(week_offset.value + 1);
|
||||
const previous_week = async () => fetchWeek(week_offset.value - 1);
|
||||
|
||||
return { week_offset, this_week, next_week, previous_week, fetch_week};
|
||||
}
|
||||
const saveTimesheetShifts = async (shifts: CreateShiftPayload[]) => {
|
||||
const email = auth_store.user?.email;
|
||||
if(!email || shifts.length === 0) return;
|
||||
await timesheet_store.createTimesheetShifts(email, shifts, week_offset.value);
|
||||
};
|
||||
|
||||
const weekStart = (date: Date) => {
|
||||
const x = new Date(date);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
x.setDate(x.getDate() - x.getDay());
|
||||
return x;
|
||||
};
|
||||
|
||||
const getCurrentWeekTimesheetOverview = async (when: Date = new Date()) => {
|
||||
const off = Math.trunc((weekStart(when).getTime() - weekStart(new Date()).getTime()) / 604800000);
|
||||
await fetchWeek(off);
|
||||
}
|
||||
|
||||
return {
|
||||
week_offset,
|
||||
fetchWeek,
|
||||
this_week,
|
||||
next_week,
|
||||
previous_week,
|
||||
saveTimesheetShifts,
|
||||
getCurrentWeekTimesheetOverview,
|
||||
};
|
||||
};
|
||||
|
|
@ -5,11 +5,12 @@ import { useTimesheetApi } from '../composables/use-timesheet-api';
|
|||
import { computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import TimesheetShiftForm from '../components/timesheet/timesheet-shift-form.vue';
|
||||
import type { CreateShiftPayload } from '../types/timesheet-shift-interface';
|
||||
|
||||
|
||||
const { locale } = useI18n();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const { this_week } = useTimesheetApi();
|
||||
const { this_week, saveTimesheetShifts } = useTimesheetApi();
|
||||
|
||||
onMounted(async () => {
|
||||
await this_week();
|
||||
|
|
@ -23,7 +24,6 @@ import TimesheetShiftForm from '../components/timesheet/timesheet-shift-form.vue
|
|||
|
||||
const timesheet_label = computed(() => {
|
||||
const dates = timesheet_store.current_timesheet.label.split('.');
|
||||
console.log(dates);
|
||||
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
|
||||
const end_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
|
||||
|
||||
|
|
@ -35,6 +35,10 @@ import TimesheetShiftForm from '../components/timesheet/timesheet-shift-form.vue
|
|||
}
|
||||
return { start_date, end_date };
|
||||
});
|
||||
|
||||
const onSaveShifts = async (payload: CreateShiftPayload[]) => {
|
||||
await saveTimesheetShifts(payload);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
|
@ -57,6 +61,9 @@ import TimesheetShiftForm from '../components/timesheet/timesheet-shift-form.vue
|
|||
{{ timesheet_label.end_date }}
|
||||
</div>
|
||||
</div>
|
||||
<TimesheetShiftForm/>
|
||||
<q-card class="q-mt-md">
|
||||
<q-inner-loading :showing="timesheet_store.is_loading"/>
|
||||
<TimesheetShiftForm @save="onSaveShifts"/>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type {Timesheet} from "src/modules/timesheets/types/timesheet-interface";
|
||||
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/timesheet-shift-interface";
|
||||
|
||||
export const timesheetTempService = {
|
||||
//GET
|
||||
getTimesheetsByEmail: async ( email: string, offset = 0): Promise<Timesheet> => {
|
||||
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`, {params: offset ? { offset } : undefined});
|
||||
return response.data as Timesheet;
|
||||
},
|
||||
|
||||
//POST
|
||||
createTimesheetShifts: async ( email: string, shifts: CreateShiftPayload[], offset = 0): Promise<Timesheet> => {
|
||||
const payload: CreateWeekShiftPayload = { shifts };
|
||||
const response = await api.post(`/timesheets/shifts/${encodeURIComponent(email)}`, payload, { params: offset ? { offset }: undefined });
|
||||
return response.data as Timesheet;
|
||||
}
|
||||
};
|
||||
|
|
@ -16,7 +16,6 @@ type Shifts = {
|
|||
is_approved: boolean;
|
||||
}
|
||||
|
||||
|
||||
type Expenses = {
|
||||
bank_type: string;
|
||||
date: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
export interface Shift {
|
||||
export type CreateShiftPayload = {
|
||||
date: string;
|
||||
type: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type CreateWeekShiftPayload = {
|
||||
shifts: CreateShiftPayload[];
|
||||
}
|
||||
|
||||
export type Shift = {
|
||||
is_approved: boolean;
|
||||
start: string;
|
||||
end: string;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
||||
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||
import type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface";
|
||||
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
||||
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
|
||||
import type { Timesheet } from 'src/modules/timesheets/types/timesheet-interface';
|
||||
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
||||
import type { CreateShiftPayload } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
||||
|
||||
const default_pay_period: PayPeriod = {
|
||||
pay_period_no: -1,
|
||||
|
|
@ -113,6 +114,18 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
is_loading.value = false;
|
||||
}
|
||||
}
|
||||
//employee timesheet
|
||||
const createTimesheetShifts = async (employee_email: string, shifts: CreateShiftPayload[], offset = 0) => {
|
||||
is_loading.value = true;
|
||||
try{
|
||||
const timesheet = await timesheetTempService.createTimesheetShifts(employee_email, shifts, offset);
|
||||
current_timesheet.value = timesheet;
|
||||
} catch (err) {
|
||||
console.error('createTimesheetShifts error: ', err);
|
||||
} finally {
|
||||
is_loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
|
||||
is_loading.value = true;
|
||||
|
|
@ -159,6 +172,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
is_loading,
|
||||
getPayPeriodByDate,
|
||||
getTimesheetByEmail,
|
||||
createTimesheetShifts,
|
||||
getPayPeriodByYearAndPeriodNumber,
|
||||
getTimesheetApprovalPayPeriodEmployeeOverviews,
|
||||
getTimesheetsByPayPeriodAndEmail,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user