635 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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" v-if="signedState !== 'signed'">
<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 && signedState !== 'signed'"
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 && signedState !== 'signed'"
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 && signedState !== 'signed'"
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 && signedState !== 'signed'"
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" v-if="signedState !== 'signed'">
<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"
v-if="signedState !== 'signed'"
>
<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">
<div class="row">
<div class="col-4" v-if="signedState !== 'signed'">
<button
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@click="sign"
>
{{ $t("sign") }}
</button>
</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>
</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(page.value);
};
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);
};
const init = () => downloadAndOpen().then(initPdf);
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));
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 && signedState.value !== "signed") {
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<null, 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;
init();
</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>