mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'signature-app/zoom-button' into 'signature-app-master'
Signature app/zoom button See merge request Chill-Projet/chill-bundles!751
This commit is contained in:
commit
9526d016c6
@ -130,3 +130,12 @@ export interface CheckSignature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasEvent = "select" | "add";
|
export type CanvasEvent = "select" | "add";
|
||||||
|
|
||||||
|
export interface ZoomLevel {
|
||||||
|
id: number;
|
||||||
|
zoom: number;
|
||||||
|
label: {
|
||||||
|
fr?: string,
|
||||||
|
nl?: string
|
||||||
|
};
|
||||||
|
}
|
@ -28,9 +28,21 @@
|
|||||||
</teleport>
|
</teleport>
|
||||||
<div class="col-12 m-auto">
|
<div class="col-12 m-auto">
|
||||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||||
<div v-if="pageCount > 1" class="col text-center turn-page">
|
<div class="col text-center turn-page">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
id="zoomSelect"
|
||||||
|
v-model="zoomLevel"
|
||||||
|
@change="setZoomLevel(zoomLevel)"
|
||||||
|
>
|
||||||
|
<option value="" selected disabled>Zoom</option>
|
||||||
|
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||||
|
{{ z.label.fr }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<template v-if="pageCount > 1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-xs p-1"
|
||||||
:disabled="page <= 1"
|
:disabled="page <= 1"
|
||||||
@click="turnPage(-1)"
|
@click="turnPage(-1)"
|
||||||
>
|
>
|
||||||
@ -38,14 +50,18 @@
|
|||||||
</button>
|
</button>
|
||||||
<span>{{ page }}/{{ pageCount }}</span>
|
<span>{{ page }}/{{ pageCount }}</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-xs p-1"
|
||||||
:disabled="page >= pageCount"
|
:disabled="page >= pageCount"
|
||||||
@click="turnPage(1)"
|
@click="turnPage(1)"
|
||||||
>
|
>
|
||||||
❯
|
❯
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
<div
|
||||||
|
v-if="signature.zones.length > 1"
|
||||||
|
class="col-5 p-0 text-center turnSignature"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@ -53,8 +69,7 @@
|
|||||||
>
|
>
|
||||||
{{ $t("last_zone") }}
|
{{ $t("last_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<span>|</span>
|
||||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@ -63,7 +78,7 @@
|
|||||||
{{ $t("next_zone") }}
|
{{ $t("next_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-end p-0">
|
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||||
<button
|
<button
|
||||||
class="btn btn-misc btn-sm"
|
class="btn btn-misc btn-sm"
|
||||||
:hidden="!userSignatureZone"
|
:hidden="!userSignatureZone"
|
||||||
@ -81,8 +96,6 @@
|
|||||||
>
|
>
|
||||||
{{ $t("cancel") }}
|
{{ $t("cancel") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="col-1" v-if="signedState !== 'signed'">
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-create btn-sm"
|
class="btn btn-create btn-sm"
|
||||||
:class="{ active: canvasEvent === 'add' }"
|
:class="{ active: canvasEvent === 'add' }"
|
||||||
@ -95,9 +108,21 @@
|
|||||||
<div
|
<div
|
||||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
||||||
>
|
>
|
||||||
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
|
<div class="col-3 text-center turn-page ps-3">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
id="zoomSelect"
|
||||||
|
v-model="zoomLevel"
|
||||||
|
@change="setZoomLevel(zoomLevel)"
|
||||||
|
>
|
||||||
|
<option value="" selected disabled>Zoom</option>
|
||||||
|
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||||
|
{{ z.label.fr }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<template v-if="pageCount > 1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-xs p-1"
|
||||||
:disabled="page <= 1"
|
:disabled="page <= 1"
|
||||||
@click="turnPage(-1)"
|
@click="turnPage(-1)"
|
||||||
>
|
>
|
||||||
@ -105,16 +130,17 @@
|
|||||||
</button>
|
</button>
|
||||||
<span>{{ page }} / {{ pageCount }}</span>
|
<span>{{ page }} / {{ pageCount }}</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-xs p-1"
|
||||||
:disabled="page >= pageCount"
|
:disabled="page >= pageCount"
|
||||||
@click="turnPage(1)"
|
@click="turnPage(1)"
|
||||||
>
|
>
|
||||||
❯
|
❯
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||||
class="col text-end d-xl-none"
|
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||||
@ -123,11 +149,7 @@
|
|||||||
>
|
>
|
||||||
{{ $t("last_zone") }}
|
{{ $t("last_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<span>|</span>
|
||||||
<div
|
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-start d-xl-none"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@ -138,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||||
class="col text-end d-none d-xl-flex p-0"
|
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||||
@ -147,11 +169,7 @@
|
|||||||
>
|
>
|
||||||
{{ $t("last_sign_zone") }}
|
{{ $t("last_sign_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<span>|</span>
|
||||||
<div
|
|
||||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
|
||||||
class="col text-start d-none d-xl-flex p-0"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
@ -160,7 +178,7 @@
|
|||||||
{{ $t("next_sign_zone") }}
|
{{ $t("next_sign_zone") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-end p-0" v-if="signedState !== 'signed'">
|
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||||
<button
|
<button
|
||||||
class="btn btn-misc btn-sm"
|
class="btn btn-misc btn-sm"
|
||||||
:hidden="!userSignatureZone"
|
:hidden="!userSignatureZone"
|
||||||
@ -177,11 +195,6 @@
|
|||||||
>
|
>
|
||||||
{{ $t("cancel") }}
|
{{ $t("cancel") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="col text-end p-0 pe-2 pe-xxl-4"
|
|
||||||
v-if="signedState !== 'signed'"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-create btn-sm"
|
class="btn btn-create btn-sm"
|
||||||
:class="{ active: canvasEvent === 'add' }"
|
:class="{ active: canvasEvent === 'add' }"
|
||||||
@ -199,7 +212,19 @@
|
|||||||
|
|
||||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-4" v-if="signedState !== 'signed'">
|
<div class="col d-flex">
|
||||||
|
<a
|
||||||
|
class="btn btn-cancel"
|
||||||
|
v-if="signedState !== 'signed'"
|
||||||
|
:href="getReturnPath()"
|
||||||
|
>
|
||||||
|
{{ $t("cancel") }}
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||||
|
{{ $t("return") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||||
<button
|
<button
|
||||||
class="btn btn-action me-2"
|
class="btn btn-action me-2"
|
||||||
:disabled="!userSignatureZone"
|
:disabled="!userSignatureZone"
|
||||||
@ -209,18 +234,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4" v-else></div>
|
<div class="col-4" v-else></div>
|
||||||
<div class="col-8 d-flex justify-content-end">
|
|
||||||
<a
|
|
||||||
class="btn btn-delete"
|
|
||||||
v-if="signedState !== 'signed'"
|
|
||||||
:href="getReturnPath()"
|
|
||||||
>
|
|
||||||
{{ $t("cancel_signing") }}
|
|
||||||
</a>
|
|
||||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
|
||||||
{{ $t("return") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -235,6 +248,7 @@ import {
|
|||||||
Signature,
|
Signature,
|
||||||
SignatureZone,
|
SignatureZone,
|
||||||
SignedState,
|
SignedState,
|
||||||
|
ZoomLevel,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from "pdfjs-dist";
|
||||||
@ -262,6 +276,52 @@ const canvasEvent: Ref<CanvasEvent> = ref("select");
|
|||||||
const signedState: Ref<SignedState> = ref("pending");
|
const signedState: Ref<SignedState> = ref("pending");
|
||||||
const page: Ref<number> = ref(1);
|
const page: Ref<number> = ref(1);
|
||||||
const pageCount: Ref<number> = ref(0);
|
const pageCount: Ref<number> = ref(0);
|
||||||
|
const zoom: Ref<number> = ref(1);
|
||||||
|
const zoomLevel = "";
|
||||||
|
const zoomLevels: Ref<ZoomLevel[]> = ref([
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
zoom: 0.75,
|
||||||
|
label: {
|
||||||
|
fr: "75%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
zoom: zoom.value,
|
||||||
|
label: {
|
||||||
|
fr: "100%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
zoom: 1.25,
|
||||||
|
label: {
|
||||||
|
fr: "125%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
zoom: 1.5,
|
||||||
|
label: {
|
||||||
|
fr: "150%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
zoom: 2,
|
||||||
|
label: {
|
||||||
|
fr: "200%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
zoom: 3,
|
||||||
|
label: {
|
||||||
|
fr: "300%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||||
let pdf = {} as PDFDocumentProxy;
|
let pdf = {} as PDFDocumentProxy;
|
||||||
|
|
||||||
@ -275,7 +335,11 @@ const $toast = useToast();
|
|||||||
|
|
||||||
const signature = window.signature;
|
const signature = window.signature;
|
||||||
|
|
||||||
console.log(signature);
|
const setZoomLevel = (zoomLevel: string) => {
|
||||||
|
zoom.value = Number.parseFloat(zoomLevel);
|
||||||
|
setPage(page.value);
|
||||||
|
setTimeout(() => drawAllZones(page.value), 200);
|
||||||
|
};
|
||||||
|
|
||||||
const mountPdf = async (doc: ArrayBuffer) => {
|
const mountPdf = async (doc: ArrayBuffer) => {
|
||||||
const loadingTask = pdfjsLib.getDocument(doc);
|
const loadingTask = pdfjsLib.getDocument(doc);
|
||||||
@ -285,7 +349,7 @@ const mountPdf = async (doc: ArrayBuffer) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||||
const scale = 1;
|
const scale = 1 * zoom.value;
|
||||||
const viewport = pdfPage.getViewport({ scale });
|
const viewport = pdfPage.getViewport({ scale });
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
@ -343,12 +407,12 @@ const hitSignature = (
|
|||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||||||
xy[0] <
|
xy[0] <
|
||||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||||||
zone.PDFPage.height -
|
zone.PDFPage.height * zoom.value -
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
||||||
xy[1] &&
|
xy[1] &&
|
||||||
xy[1] <
|
xy[1] <
|
||||||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
||||||
zone.PDFPage.height;
|
zone.PDFPage.height * zoom.value;
|
||||||
|
|
||||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||||
userSignatureZone.value = z;
|
userSignatureZone.value = z;
|
||||||
@ -425,19 +489,19 @@ const drawZone = (
|
|||||||
ctx.lineJoin = "bevel";
|
ctx.lineJoin = "bevel";
|
||||||
ctx.strokeRect(
|
ctx.strokeRect(
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||||||
zone.PDFPage.height -
|
zone.PDFPage.height * zoom.value -
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
||||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
||||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
||||||
);
|
);
|
||||||
ctx.font = "bold 16px serif";
|
ctx.font = `bold ${16 * zoom.value}px serif`;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
const xText =
|
const xText =
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
||||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
||||||
const yText =
|
const yText =
|
||||||
zone.PDFPage.height -
|
zone.PDFPage.height * zoom.value -
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
||||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
||||||
if (userSignatureZone.value?.index === zone.index) {
|
if (userSignatureZone.value?.index === zone.index) {
|
||||||
@ -445,8 +509,8 @@ const drawZone = (
|
|||||||
ctx.fillText("Signer ici", xText, yText);
|
ctx.fillText("Signer ici", xText, yText);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = unselectedBlue;
|
ctx.fillStyle = unselectedBlue;
|
||||||
ctx.fillText("Choisir cette", xText, yText - 12);
|
ctx.fillText("Choisir cette", xText, yText - 12 * zoom.value);
|
||||||
ctx.fillText("zone de signature", xText, yText + 12);
|
ctx.fillText("zone de signature", xText, yText + 12 * zoom.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -616,16 +680,29 @@ div#action-buttons {
|
|||||||
}
|
}
|
||||||
div.pdf-tools {
|
div.pdf-tools {
|
||||||
background-color: #f3f3f3;
|
background-color: #f3f3f3;
|
||||||
font-size: 0.8rem;
|
font-size: 0.6rem;
|
||||||
|
button {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
}
|
||||||
|
div.turnSignature {
|
||||||
|
span {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 1400px) {
|
@media (min-width: 1400px) {
|
||||||
// background: none;
|
// background: none;
|
||||||
// border: none !important;
|
// border: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div.turn-page {
|
div.turn-page {
|
||||||
|
display: flex;
|
||||||
span {
|
span {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
margin: 0 0.4rem;
|
margin: auto 0.4rem;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
width: 5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div.signature-modal-body {
|
div.signature-modal-body {
|
||||||
|
@ -12,7 +12,6 @@ const appMessages = {
|
|||||||
sign: 'Signer',
|
sign: 'Signer',
|
||||||
choose_another_signature: 'Choisir une autre zone',
|
choose_another_signature: 'Choisir une autre zone',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
cancel_signing: 'Refuser de signer',
|
|
||||||
last_sign_zone: 'Zone de signature précédente',
|
last_sign_zone: 'Zone de signature précédente',
|
||||||
next_sign_zone: 'Zone de signature suivante',
|
next_sign_zone: 'Zone de signature suivante',
|
||||||
add_sign_zone: 'Ajouter une zone de signature',
|
add_sign_zone: 'Ajouter une zone de signature',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user