882 lines
28 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 sticky-top">
<div
class="row justify-content-center border-bottom pdf-tools d-md-none"
>
<div class="col-5 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
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
</template>
<template v-if="pageCount > 1">
<button
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
<input
type="checkbox"
id="checkboxMulti"
v-model="multiPage"
@change="toggleMultiPage"
/>
<label class="form-check-label" for="checkboxMulti">
{{ $t("all_pages") }}
</label>
</template>
</div>
<div
v-if="signature.zones.length > 0"
class="col-5 p-0 text-center turnSignature"
>
<button
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_zone") }}
</button>
<span>|</span>
<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" v-if="signedState !== 'signed'">
<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>
<button
v-if="userSignatureZone === null"
:class="{
btn: true,
'btn-sm': true,
'btn-create': canvasEvent !== 'add',
'btn-chill-green': canvasEvent === 'add',
active: canvasEvent === 'add',
}"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
>
<template v-if="canvasEvent === 'add'">
<div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</template>
</button>
</div>
</div>
<div
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
>
<div class="col-5 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
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }} / {{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
<input
type="checkbox"
id="checkboxMulti"
v-model="multiPage"
@change="toggleMultiPage"
/>
<label class="form-check-label" for="checkboxMulti">
{{ $t("see_all_pages") }}
</label>
</template>
</div>
<div
v-if="signature.zones.length > 0 && signedState !== 'signed'"
class="col-4 d-xl-none text-center turnSignature p-0"
>
<button
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_zone") }}
</button>
<span>|</span>
<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 > 0 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
>
<button
:disabled="
userSignatureZone === null ||
userSignatureZone?.index < 1
"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_sign_zone") }}
</button>
<span>|</span>
<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" 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>
<button
v-if="userSignatureZone === null"
:class="{
btn: true,
'btn-sm': true,
'btn-create': canvasEvent !== 'add',
'btn-chill-green': canvasEvent === 'add',
active: canvasEvent === 'add',
}"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
>
<template v-if="canvasEvent !== 'add'">
{{ $t("add_zone") }}
</template>
<template v-else>
{{ $t("click_on_document") }}
<div
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
</template>
</button>
</div>
</div>
</div>
<div
v-if="multiPage"
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center d-flex flex-column"
:class="{ onAddZone: canvasEvent === 'add' }"
>
<canvas v-for="p in pageCount" :key="p" :id="`canvas-${p}`"></canvas>
</div>
<div
v-else
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center"
:class="{ onAddZone: canvasEvent === 'add' }"
>
<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 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
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@click="sign"
>
{{ $t("sign") }}
</button>
</div>
<div class="col-4" v-else></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,
ZoomLevel,
} 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,
download_doc_as_pdf,
} from "../StoredObjectButton/helpers";
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
const multiPage: Ref<boolean> = ref(true);
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);
const zoom: Ref<number> = ref(1);
let 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 pdf = {} as PDFDocumentProxy;
declare global {
interface Window {
signature: Signature;
}
}
const $toast = useToast();
const signature = window.signature;
const setZoomLevel = async (zoomLevel: string) => {
zoom.value = Number.parseFloat(zoomLevel);
await resetPages();
setTimeout(drawAllZones, 200);
};
const mountPdf = async (doc: ArrayBuffer) => {
const loadingTask = pdfjsLib.getDocument(doc);
pdf = await loadingTask.promise;
pageCount.value = pdf.numPages;
if (multiPage.value) {
await setAllPages();
} else {
await setPage(1);
}
};
const getCanvas = (page: number) =>
multiPage.value
? (document.getElementById(`canvas-${page}`) as HTMLCanvasElement)
: (document.querySelectorAll("canvas")[0] as HTMLCanvasElement);
const getCanvasId = (canvas: HTMLCanvasElement) => {
const number = canvas.id.split("-").pop();
return number ? parseInt(number) : 0;
};
const getRenderContext = (pdfPage: PDFPageProxy) => {
const scale = 1 * zoom.value;
const viewport = pdfPage.getViewport({ scale });
const canvas = getCanvas(pdfPage.pageNumber);
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
canvas.height = viewport.height;
canvas.width = viewport.width;
return {
canvasContext: context,
viewport: viewport,
};
};
const setAllPages = async () =>
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
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_doc_as_pdf(signature.storedObject);
} catch (e) {
console.error("error while downloading and decrypting document", e);
throw e;
}
const doc = await raw.arrayBuffer();
await mountPdf(doc);
return raw;
}
const addCanvasEvents = () => {
if (multiPage.value) {
Array.from(Array(pageCount.value).keys()).map((p) => {
const canvas = getCanvas(p + 1);
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false,
);
});
} else {
const canvas = document.querySelectorAll(
"canvas",
)[0] as HTMLCanvasElement;
canvas.addEventListener(
"pointerup",
(e) => canvasClick(e, canvas),
false,
);
}
};
const initPdf = () => {
addCanvasEvents();
setTimeout(drawAllZones, 800);
};
const resetPages = () =>
multiPage.value ? setAllPages() : setPage(page.value);
const toggleMultiPage = async () => {
await resetPages();
setTimeout(drawAllZones, 200);
addCanvasEvents();
};
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[],
canvas: HTMLCanvasElement,
) =>
scaleXToCanvas(zone.x, canvas.width, zone.PDFPage.width) < xy[0] &&
xy[0] <
scaleXToCanvas(zone.x + zone.width, canvas.width, zone.PDFPage.width) &&
zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
xy[1] &&
xy[1] <
scaleYToCanvas(
zone.height - zone.y,
canvas.height,
zone.PDFPage.height,
) +
zone.PDFPage.height * zoom.value;
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
userSignatureZone.value = z;
const ctx = canvas.getContext("2d");
if (ctx) {
await resetPages();
setTimeout(drawAllZones, 200);
}
};
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones
.filter(
(z) =>
(z.PDFPage.index + 1 === getCanvasId(canvas) &&
multiPage.value) ||
(z.PDFPage.index + 1 === page.value && !multiPage.value),
)
.map((z) => {
if (hitSignature(z, [e.offsetX, e.offsetY], canvas)) {
if (userSignatureZone.value === null) {
selectZone(z, canvas);
} else {
if (userSignatureZone.value.index === z.index) {
sign();
}
}
}
});
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
canvasEvent.value === "select"
? selectZoneEvent(e, canvas)
: addZoneEvent(e, canvas);
const turnPage = async (upOrDown: number) => {
page.value = page.value + upOrDown;
if (multiPage.value) {
const canvas = getCanvas(page.value);
canvas.scrollIntoView();
} else {
await setPage(page.value);
setTimeout(drawAllZones, 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;
const canvas = getCanvas(currentZone.PDFPage.index + 1);
selectZone(currentZone, canvas);
canvas.scrollIntoView();
}
};
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 * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height),
);
ctx.font = `bold ${16 * zoom.value}px 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 * zoom.value -
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 * zoom.value);
ctx.fillText("zone de signature", xText, yText + 12 * zoom.value);
}
};
const drawAllZones = () => {
if (signedState.value !== "signed") {
signature.zones
.filter(
(z) =>
multiPage.value ||
(z.PDFPage.index + 1 === page.value && !multiPage.value),
)
.map((z) => {
const canvas = getCanvas(z.PDFPage.index + 1);
const ctx = canvas.getContext("2d");
if (ctx) {
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 resetPages();
setTimeout(drawAllZones, 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 * zoom.value -
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
width: BOX_WIDTH * zoom.value,
height: BOX_HEIGHT * zoom.value,
PDFPage: {
index: multiPage.value ? getCanvasId(canvas) - 1 : page.value - 1,
width: PDFPageWidth,
height: PDFPageHeight,
},
};
signature.zones.push(newZone);
userSignatureZone.value = newZone;
await resetPages();
setTimeout(drawAllZones, 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);
margin: 1rem auto;
}
.onAddZone {
cursor: not-allowed;
canvas {
cursor: copy;
}
}
div#action-buttons {
position: sticky;
bottom: 0px;
background-color: white;
z-index: 100;
}
div.pdf-tools {
background-color: #f3f3f3;
font-size: 0.6rem;
label {
font-size: 0.75rem !important;
margin: auto 0 auto 0.3rem;
}
button {
font-size: 0.75rem !important;
}
div.turnSignature {
span {
font-size: 1rem;
}
}
@media (min-width: 1400px) {
// background: none;
// border: none !important;
}
}
div.turn-page {
display: flex;
span {
font-size: 0.75rem;
margin: auto 0.4rem;
}
select {
width: 5rem;
font-size: 0.75rem;
}
}
div.signature-modal-body {
height: 8rem;
}
</style>