mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
627 lines
17 KiB
Vue
627 lines
17 KiB
Vue
<template>
|
||
<teleport to="body">
|
||
<modal v-if="modalOpen" @close="modalOpen = false">
|
||
<template v-slot:header>
|
||
<h2>{{ $t("signature_confirmation") }}</h2>
|
||
</template>
|
||
<template v-slot:body>
|
||
<div class="signature-modal-body text-center" v-if="loading">
|
||
<p>{{ $t("electronic_signature_in_progress") }}</p>
|
||
<div class="loading">
|
||
<i
|
||
class="fa fa-circle-o-notch fa-spin fa-3x"
|
||
:title="$t('loading')"
|
||
></i>
|
||
</div>
|
||
</div>
|
||
<div class="signature-modal-body text-center" v-else>
|
||
<p>{{ $t("you_are_going_to_sign") }}</p>
|
||
<p>{{ $t("are_you_sure") }}</p>
|
||
</div>
|
||
</template>
|
||
<template v-slot:footer>
|
||
<button class="btn btn-action" @click.prevent="confirmSign">
|
||
{{ $t("yes") }}
|
||
</button>
|
||
</template>
|
||
</modal>
|
||
</teleport>
|
||
<div class="col-12 m-auto">
|
||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||
<div v-if="pageCount > 1" class="col text-center turn-page">
|
||
<button
|
||
class="btn btn-light btn-sm"
|
||
:disabled="page <= 1"
|
||
@click="turnPage(-1)"
|
||
>
|
||
❮
|
||
</button>
|
||
<span>{{ page }}/{{ pageCount }}</span>
|
||
<button
|
||
class="btn btn-light btn-sm"
|
||
:disabled="page >= pageCount"
|
||
@click="turnPage(1)"
|
||
>
|
||
❯
|
||
</button>
|
||
</div>
|
||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
||
<button
|
||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(-1)"
|
||
>
|
||
{{ $t("last_zone") }}
|
||
</button>
|
||
</div>
|
||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
||
<button
|
||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(1)"
|
||
>
|
||
{{ $t("next_zone") }}
|
||
</button>
|
||
</div>
|
||
<div class="col text-end p-0">
|
||
<button
|
||
class="btn btn-misc btn-sm"
|
||
:hidden="!userSignatureZone"
|
||
@click="undoSign"
|
||
v-if="signature.zones.length > 1"
|
||
:title="$t('choose_another_signature')"
|
||
>
|
||
{{ $t("another_zone") }}
|
||
</button>
|
||
<button
|
||
class="btn btn-misc btn-sm"
|
||
:hidden="!userSignatureZone"
|
||
@click="undoSign"
|
||
v-else
|
||
>
|
||
{{ $t("cancel") }}
|
||
</button>
|
||
</div>
|
||
<div class="col-1">
|
||
<button
|
||
class="btn btn-create btn-sm"
|
||
:class="{ active: canvasEvent === 'add' }"
|
||
@click="toggleAddZone()"
|
||
:title="$t('add_sign_zone')"
|
||
></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
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">
|
||
<button
|
||
class="btn btn-light btn-sm"
|
||
:disabled="page <= 1"
|
||
@click="turnPage(-1)"
|
||
>
|
||
❮
|
||
</button>
|
||
<span>{{ page }} / {{ pageCount }}</span>
|
||
<button
|
||
class="btn btn-light btn-sm"
|
||
:disabled="page >= pageCount"
|
||
@click="turnPage(1)"
|
||
>
|
||
❯
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="signature.zones.length > 1"
|
||
class="col text-end d-xl-none"
|
||
>
|
||
<button
|
||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(-1)"
|
||
>
|
||
{{ $t("last_zone") }}
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="signature.zones.length > 1"
|
||
class="col text-start d-xl-none"
|
||
>
|
||
<button
|
||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(1)"
|
||
>
|
||
{{ $t("next_zone") }}
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="signature.zones.length > 1"
|
||
class="col text-end d-none d-xl-flex p-0"
|
||
>
|
||
<button
|
||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(-1)"
|
||
>
|
||
{{ $t("last_sign_zone") }}
|
||
</button>
|
||
</div>
|
||
<div
|
||
v-if="signature.zones.length > 1"
|
||
class="col text-start d-none d-xl-flex p-0"
|
||
>
|
||
<button
|
||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||
class="btn btn-light btn-sm"
|
||
@click="turnSignature(1)"
|
||
>
|
||
{{ $t("next_sign_zone") }}
|
||
</button>
|
||
</div>
|
||
<div class="col text-end p-0">
|
||
<button
|
||
class="btn btn-misc btn-sm"
|
||
:hidden="!userSignatureZone"
|
||
@click="undoSign"
|
||
v-if="signature.zones.length > 1"
|
||
>
|
||
{{ $t("choose_another_signature") }}
|
||
</button>
|
||
<button
|
||
class="btn btn-misc btn-sm"
|
||
:hidden="!userSignatureZone"
|
||
@click="undoSign"
|
||
v-else
|
||
>
|
||
{{ $t("cancel") }}
|
||
</button>
|
||
</div>
|
||
<div class="col text-end p-0 pe-2 pe-xxl-4">
|
||
<button
|
||
class="btn btn-create btn-sm"
|
||
:class="{ active: canvasEvent === 'add' }"
|
||
@click="toggleAddZone()"
|
||
:title="$t('add_sign_zone')"
|
||
>
|
||
{{ $t("add_zone") }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
|
||
<canvas class="m-auto" id="canvas"></canvas>
|
||
</div>
|
||
|
||
<div
|
||
class="col-xs-12 col-md-12 col-lg-9 m-auto p-4"
|
||
id="action-buttons"
|
||
v-if="signedState !== 'signed'"
|
||
>
|
||
<div class="row">
|
||
<div class="col-4">
|
||
<button
|
||
class="btn btn-action me-2"
|
||
:disabled="!userSignatureZone"
|
||
@click="sign"
|
||
>
|
||
{{ $t("sign") }}
|
||
</button>
|
||
</div>
|
||
<div class="col-8 d-flex justify-content-end">
|
||
<a class="btn btn-delete" :href="getReturnPath()">
|
||
{{ $t("cancel_signing") }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, Ref, reactive } from "vue";
|
||
import { useToast } from "vue-toast-notification";
|
||
import "vue-toast-notification/dist/theme-sugar.css";
|
||
import {
|
||
CanvasEvent,
|
||
CheckSignature,
|
||
Signature,
|
||
SignatureZone,
|
||
SignedState,
|
||
} from "../../types";
|
||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||
import * as pdfjsLib from "pdfjs-dist";
|
||
import {
|
||
PDFDocumentProxy,
|
||
PDFPageProxy,
|
||
} from "pdfjs-dist/types/src/display/api";
|
||
|
||
// @ts-ignore
|
||
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
|
||
console.log(PdfWorker); // incredible but this is needed
|
||
|
||
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
|
||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||
|
||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
|
||
|
||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||
|
||
const modalOpen: Ref<boolean> = ref(false);
|
||
const loading: Ref<boolean> = ref(false);
|
||
const adding: Ref<boolean> = ref(false);
|
||
const canvasEvent: Ref<CanvasEvent> = ref("select");
|
||
const signedState: Ref<SignedState> = ref("pending");
|
||
const page: Ref<number> = ref(1);
|
||
const pageCount: Ref<number> = ref(0);
|
||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||
let pdf = {} as PDFDocumentProxy;
|
||
|
||
declare global {
|
||
interface Window {
|
||
signature: Signature;
|
||
}
|
||
}
|
||
|
||
const $toast = useToast();
|
||
|
||
const signature = window.signature;
|
||
|
||
console.log(signature);
|
||
|
||
const mountPdf = async (url: string) => {
|
||
const loadingTask = pdfjsLib.getDocument(url);
|
||
pdf = await loadingTask.promise;
|
||
pageCount.value = pdf.numPages;
|
||
await setPage(1);
|
||
};
|
||
|
||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||
const scale = 1;
|
||
const viewport = pdfPage.getViewport({ scale });
|
||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||
canvas.height = viewport.height;
|
||
canvas.width = viewport.width;
|
||
|
||
return {
|
||
canvasContext: context,
|
||
viewport: viewport,
|
||
};
|
||
};
|
||
|
||
const setPage = async (page: number) => {
|
||
const pdfPage = await pdf.getPage(page);
|
||
const renderContext = getRenderContext(pdfPage);
|
||
await pdfPage.render(renderContext);
|
||
};
|
||
|
||
async function downloadAndOpen(): Promise<Blob> {
|
||
let raw;
|
||
try {
|
||
raw = await download_and_decrypt_doc(
|
||
signature.storedObject,
|
||
signature.storedObject.currentVersion
|
||
);
|
||
} catch (e) {
|
||
console.error("error while downloading and decrypting document", e);
|
||
throw e;
|
||
}
|
||
await mountPdf(URL.createObjectURL(raw));
|
||
initPdf();
|
||
return raw;
|
||
}
|
||
|
||
const initPdf = () => {
|
||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||
canvas.addEventListener("pointerup", canvasClick, false);
|
||
setTimeout(() => drawAllZones(page.value), 800);
|
||
};
|
||
|
||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
||
Math.round((x * canvasWidth) / PDFWidth);
|
||
|
||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
||
Math.round((h * canvasHeight) / PDFHeight);
|
||
|
||
const hitSignature = (
|
||
zone: SignatureZone,
|
||
xy: number[],
|
||
canvasWidth: number,
|
||
canvasHeight: number
|
||
) =>
|
||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||
xy[0] <
|
||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||
zone.PDFPage.height -
|
||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
||
xy[1] &&
|
||
xy[1] <
|
||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
||
zone.PDFPage.height;
|
||
|
||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||
userSignatureZone.value = z;
|
||
const ctx = canvas.getContext("2d");
|
||
if (ctx) {
|
||
setPage(page.value);
|
||
setTimeout(() => drawAllZones(page.value), 200);
|
||
}
|
||
};
|
||
|
||
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||
signature.zones
|
||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||
.map((z) => {
|
||
if (
|
||
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
|
||
) {
|
||
if (userSignatureZone.value === null) {
|
||
selectZone(z, canvas);
|
||
} else {
|
||
if (userSignatureZone.value.index === z.index) {
|
||
sign();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const canvasClick = (e: PointerEvent) => {
|
||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||
canvasEvent.value === "select"
|
||
? selectZoneEvent(e, canvas)
|
||
: addZoneEvent(e, canvas);
|
||
};
|
||
|
||
const turnPage = async (upOrDown: number) => {
|
||
//userSignatureZone.value = null; // desactivate the reset of the zone when turning page
|
||
page.value = page.value + upOrDown;
|
||
await setPage(page.value);
|
||
setTimeout(() => drawAllZones(page.value), 200);
|
||
};
|
||
|
||
const turnSignature = async (upOrDown: number) => {
|
||
let zoneIndex = userSignatureZone.value?.index ?? -1;
|
||
if (zoneIndex < -1) {
|
||
zoneIndex = -1;
|
||
}
|
||
if (zoneIndex < signature.zones.length) {
|
||
zoneIndex = zoneIndex + upOrDown;
|
||
} else {
|
||
zoneIndex = 0;
|
||
}
|
||
let currentZone = signature.zones[zoneIndex];
|
||
if (currentZone) {
|
||
page.value = currentZone.PDFPage.index + 1;
|
||
userSignatureZone.value = currentZone;
|
||
const canvas = document.querySelectorAll("canvas")[0];
|
||
selectZone(currentZone, canvas);
|
||
}
|
||
};
|
||
|
||
const drawZone = (
|
||
zone: SignatureZone,
|
||
ctx: CanvasRenderingContext2D,
|
||
canvasWidth: number,
|
||
canvasHeight: number
|
||
) => {
|
||
const unselectedBlue = "#007bff";
|
||
const selectedBlue = "#034286";
|
||
ctx.strokeStyle =
|
||
userSignatureZone.value?.index === zone.index
|
||
? selectedBlue
|
||
: unselectedBlue;
|
||
ctx.lineWidth = 2;
|
||
ctx.lineJoin = "bevel";
|
||
ctx.strokeRect(
|
||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||
zone.PDFPage.height -
|
||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
||
);
|
||
ctx.font = "bold 16px serif";
|
||
ctx.textAlign = "center";
|
||
ctx.fillStyle = "black";
|
||
const xText =
|
||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
||
const yText =
|
||
zone.PDFPage.height -
|
||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
||
if (userSignatureZone.value?.index === zone.index) {
|
||
ctx.fillStyle = selectedBlue;
|
||
ctx.fillText("Signer ici", xText, yText);
|
||
} else {
|
||
ctx.fillStyle = unselectedBlue;
|
||
ctx.fillText("Choisir cette", xText, yText - 12);
|
||
ctx.fillText("zone de signature", xText, yText + 12);
|
||
}
|
||
};
|
||
|
||
const drawAllZones = (page: number) => {
|
||
const canvas = document.querySelectorAll("canvas")[0];
|
||
const ctx = canvas.getContext("2d");
|
||
if (ctx) {
|
||
signature.zones
|
||
.filter((z) => z.PDFPage.index + 1 === page)
|
||
.map((z) => {
|
||
if (userSignatureZone.value) {
|
||
if (userSignatureZone.value?.index === z.index) {
|
||
drawZone(z, ctx, canvas.width, canvas.height);
|
||
}
|
||
} else {
|
||
drawZone(z, ctx, canvas.width, canvas.height);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
const checkSignature = () => {
|
||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||
return makeFetch<CheckSignature, CheckSignature>("GET", url)
|
||
.then((r) => {
|
||
signedState.value = r.state;
|
||
signature.storedObject = r.storedObject;
|
||
checkForReady();
|
||
})
|
||
.catch((error) => {
|
||
signedState.value = "error";
|
||
console.log("Error while checking the signature", error);
|
||
$toast.error(
|
||
`Erreur lors de la vérification de la signature: ${error.txt}`
|
||
);
|
||
});
|
||
};
|
||
|
||
const maxTryForReady = 60; //2 minutes for trying to sign
|
||
let tryForReady = 0;
|
||
|
||
const stopTrySigning = () => {
|
||
loading.value = false;
|
||
modalOpen.value = false;
|
||
};
|
||
|
||
const checkForReady = () => {
|
||
if (tryForReady > maxTryForReady) {
|
||
stopTrySigning();
|
||
tryForReady = 0;
|
||
console.log("Reached the maximum number of tentative to try signing");
|
||
$toast.error(
|
||
"Le nombre maximum de tentatives pour essayer de signer est atteint"
|
||
);
|
||
}
|
||
if (signedState.value === "rejected") {
|
||
stopTrySigning();
|
||
console.log("Signature rejected by the server");
|
||
$toast.error("Signature rejetée par le serveur");
|
||
}
|
||
if (signedState.value === "canceled") {
|
||
stopTrySigning();
|
||
console.log("Signature canceled");
|
||
$toast.error("Signature annulée");
|
||
}
|
||
if (signedState.value === "pending") {
|
||
tryForReady = tryForReady + 1;
|
||
setTimeout(() => checkSignature(), 2000);
|
||
} else {
|
||
stopTrySigning();
|
||
if (signedState.value === "signed") {
|
||
userSignatureZone.value = null;
|
||
downloadAndOpen();
|
||
}
|
||
}
|
||
};
|
||
|
||
const sign = () => (modalOpen.value = true);
|
||
|
||
const confirmSign = () => {
|
||
loading.value = true;
|
||
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
|
||
const body = {
|
||
storedObject: signature.storedObject,
|
||
zone: userSignatureZone.value,
|
||
};
|
||
makeFetch("POST", url, body)
|
||
.then((r) => {
|
||
checkForReady();
|
||
})
|
||
.catch((error) => {
|
||
console.log("Error while posting the signature", error);
|
||
stopTrySigning();
|
||
$toast.error(
|
||
`Erreur lors de la soumission de la signature: ${error.txt}`
|
||
);
|
||
});
|
||
};
|
||
|
||
const undoSign = async () => {
|
||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||
await setPage(page.value);
|
||
setTimeout(() => drawAllZones(page.value), 200);
|
||
userSignatureZone.value = null;
|
||
adding.value = false;
|
||
canvasEvent.value = "select";
|
||
};
|
||
|
||
const toggleAddZone = () => {
|
||
canvasEvent.value === "select"
|
||
? (canvasEvent.value = "add")
|
||
: (canvasEvent.value = "select");
|
||
};
|
||
|
||
const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
||
const BOX_WIDTH = 180;
|
||
const BOX_HEIGHT = 90;
|
||
const PDFPageHeight = canvas.height;
|
||
const PDFPageWidth = canvas.width;
|
||
|
||
const x = e.offsetX;
|
||
const y = e.offsetY;
|
||
const newZone: SignatureZone = {
|
||
index: null,
|
||
x:
|
||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
||
y:
|
||
PDFPageHeight -
|
||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
||
width: BOX_WIDTH,
|
||
height: BOX_HEIGHT,
|
||
PDFPage: {
|
||
index: page.value - 1,
|
||
width: PDFPageWidth,
|
||
height: PDFPageHeight,
|
||
},
|
||
};
|
||
signature.zones.push(newZone);
|
||
userSignatureZone.value = newZone;
|
||
|
||
await setPage(page.value);
|
||
setTimeout(() => drawAllZones(page.value), 200);
|
||
canvasEvent.value = "select";
|
||
adding.value = true;
|
||
};
|
||
|
||
const getReturnPath = () =>
|
||
window.location.search
|
||
? window.location.search.split("?returnPath=")[1] ??
|
||
window.location.pathname
|
||
: window.location.pathname;
|
||
|
||
downloadAndOpen();
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
#canvas {
|
||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
div#action-buttons {
|
||
position: sticky;
|
||
bottom: 0px;
|
||
background-color: white;
|
||
z-index: 100;
|
||
}
|
||
div.pdf-tools {
|
||
background-color: #f3f3f3;
|
||
font-size: 0.8rem;
|
||
@media (min-width: 1400px) {
|
||
// background: none;
|
||
// border: none !important;
|
||
}
|
||
}
|
||
div.turn-page {
|
||
span {
|
||
font-size: 0.8rem;
|
||
margin: 0 0.4rem;
|
||
}
|
||
}
|
||
div.signature-modal-body {
|
||
height: 8rem;
|
||
}
|
||
</style>
|
||
|