mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'signature-app-vueapp' into 'signature-app-master'
Signature app vueapp See merge request Chill-Projet/chill-bundles!714
This commit is contained in:
commit
64e527672d
@ -53,6 +53,7 @@
|
||||
"marked": "^12.0.2",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"mime": "^4.0.0",
|
||||
"pdfjs-dist": "^4.3.136",
|
||||
"swagger-ui": "^4.15.5",
|
||||
"vis-network": "^9.1.0",
|
||||
"vue": "^3.2.37",
|
||||
|
@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
|
||||
/**
|
||||
* Class DocumentPersonController.
|
||||
@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController
|
||||
protected TranslatorInterface $translator,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected AuthorizationHelper $authorizationHelper,
|
||||
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
protected StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
||||
) {}
|
||||
|
||||
@ -197,4 +201,36 @@ class DocumentPersonController extends AbstractController
|
||||
['document' => $document, 'person' => $person]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
|
||||
public function signature(Person $person, PersonDocument $document): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
|
||||
|
||||
$event = new PrivacyEvent($person, [
|
||||
'element_class' => PersonDocument::class,
|
||||
'element_id' => $document->getId(),
|
||||
'action' => 'show',
|
||||
]);
|
||||
$this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
|
||||
|
||||
$storedObject = $document->getObject();
|
||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||
|
||||
$signature = [];
|
||||
$signature['id'] = 1;
|
||||
$signature['storedObject'] = [ // TEMP
|
||||
'filename' => $storedObject->getFilename(),
|
||||
'iv' => $storedObject->getIv(),
|
||||
'keyInfos' => $storedObject->getKeyInfos(),
|
||||
];
|
||||
$signature['zones'] = $zones;
|
||||
|
||||
return $this->render(
|
||||
'@ChillDocStore/PersonDocument/signature.html.twig',
|
||||
['document' => $document, 'person' => $person, 'signature' => $signature]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@ -25,22 +27,41 @@ class SignatureRequestController
|
||||
public function __construct(
|
||||
private MessageBusInterface $messageBus,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||
public function processSignature(StoredObject $storedObject): Response
|
||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||
{
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
|
||||
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
|
||||
$zone = new PDFSignatureZone(
|
||||
$data['zone']['index'],
|
||||
$data['zone']['x'],
|
||||
$data['zone']['y'],
|
||||
$data['zone']['height'],
|
||||
$data['zone']['width'],
|
||||
new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
||||
0,
|
||||
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||
0,
|
||||
'test signature',
|
||||
'Mme Caroline Diallo',
|
||||
$signature->getId(),
|
||||
$zone,
|
||||
$data['zone']['index'],
|
||||
'test signature', // reason (string)
|
||||
'Mme Caroline Diallo', // signerText (string)
|
||||
$content
|
||||
));
|
||||
|
||||
return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
|
||||
return new JsonResponse(null, JsonResponse::HTTP_OK, []);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
||||
{
|
||||
return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
|
||||
}
|
||||
}
|
||||
|
@ -26,11 +26,11 @@ export interface StoredObject {
|
||||
}
|
||||
|
||||
export interface StoredObjectCreated {
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
type: string,
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
type: string,
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
@ -51,14 +51,35 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
* Object containing information for performering a POST request to a swift object store
|
||||
*/
|
||||
export interface PostStoreObjectSignature {
|
||||
method: "POST",
|
||||
max_file_size: number,
|
||||
max_file_count: 1,
|
||||
expires: number,
|
||||
submit_delay: 180,
|
||||
redirect: string,
|
||||
prefix: string,
|
||||
url: string,
|
||||
signature: string,
|
||||
method: "POST",
|
||||
max_file_size: number,
|
||||
max_file_count: 1,
|
||||
expires: number,
|
||||
submit_delay: 180,
|
||||
redirect: string,
|
||||
prefix: string,
|
||||
url: string,
|
||||
signature: string,
|
||||
}
|
||||
|
||||
export interface PDFPage {
|
||||
index: number,
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
export interface SignatureZone {
|
||||
index: number,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
PDFPage: PDFPage,
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
id: number,
|
||||
storedObject: StoredObject,
|
||||
zones: SignatureZone[],
|
||||
}
|
||||
|
||||
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
|
@ -0,0 +1,426 @@
|
||||
<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">
|
||||
<div
|
||||
class="row justify-content-center mb-2"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
<div class="col-4 gap-2 d-grid">
|
||||
<button
|
||||
:disabled="zone < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4 gap-2 d-grid">
|
||||
<button class="btn btn-light btn-sm" @click="turnSignature(1)">
|
||||
{{ $t("next_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="turn-page"
|
||||
class="row justify-content-center mb-2"
|
||||
v-if="pageCount > 1"
|
||||
>
|
||||
<div class="col-6-sm col-3-md text-center">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>page {{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<canvas class="m-auto" id="canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button
|
||||
class="btn btn-action me-2"
|
||||
:disabled="!userSignatureZones"
|
||||
@click="modalOpen = true"
|
||||
>
|
||||
{{ $t("sign") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc"
|
||||
:hidden="!userSignatureZones"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
{{ $t("choose_another_signature") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc"
|
||||
:hidden="!userSignatureZones"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button class="btn float-end btn-delete" @click="undoSign">
|
||||
{{ $t("cancel_signing") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, reactive } from "vue";
|
||||
import { 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 {
|
||||
build_download_info_link,
|
||||
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 signedState: Ref<SignedState> = ref("pending");
|
||||
const page: Ref<number> = ref(1);
|
||||
const pageCount: Ref<number> = ref(0);
|
||||
const zone: Ref<number> = ref(0);
|
||||
let userSignatureZones: Ref<null | SignatureZone> = ref(null);
|
||||
let pdfSource: Ref<string> = ref("");
|
||||
let pdf = {} as PDFDocumentProxy;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
signature: Signature;
|
||||
}
|
||||
}
|
||||
|
||||
const signature = window.signature;
|
||||
const urlInfo = build_download_info_link(signature.storedObject.filename);
|
||||
|
||||
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(
|
||||
urlInfo,
|
||||
signature.storedObject.keyInfos,
|
||||
new Uint8Array(signature.storedObject.iv)
|
||||
);
|
||||
} 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",
|
||||
(e: PointerEvent) => canvasClick(e, canvas),
|
||||
false
|
||||
);
|
||||
setTimeout(() => addZones(page.value), 800);
|
||||
};
|
||||
|
||||
const hitSignature = (
|
||||
zone: SignatureZone,
|
||||
xy: number[],
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const scaleXToCanvas = (x: number) =>
|
||||
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||
const scaleHeightToCanvas = (h: number) =>
|
||||
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||
const scaleYToCanvas = (y: number) =>
|
||||
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||
return (
|
||||
scaleXToCanvas(zone.x) < xy[0] &&
|
||||
xy[0] < scaleXToCanvas(zone.x + zone.width) &&
|
||||
scaleYToCanvas(zone.y) < xy[1] &&
|
||||
xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height)
|
||||
);
|
||||
};
|
||||
|
||||
const canvasClick = (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)
|
||||
) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
setPage(page.value);
|
||||
setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
|
||||
}
|
||||
userSignatureZones.value = z;
|
||||
}
|
||||
});
|
||||
|
||||
const turnPage = async (upOrDown: number) => {
|
||||
userSignatureZones.value = null;
|
||||
page.value = page.value + upOrDown;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
};
|
||||
|
||||
const turnSignature = async (upOrDown: number) => {
|
||||
userSignatureZones.value = null;
|
||||
if (zone.value < signature.zones.length - 1) {
|
||||
zone.value = zone.value + upOrDown;
|
||||
} else {
|
||||
zone.value = 0;
|
||||
}
|
||||
let currentZone = signature.zones[zone.value];
|
||||
if (currentZone) {
|
||||
page.value = currentZone.PDFPage.index + 1;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
}
|
||||
};
|
||||
|
||||
const drawZone = (
|
||||
zone: SignatureZone,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const unselectedBlue = "#007bff";
|
||||
const selectedBlue = "#034286";
|
||||
const scaleXToCanvas = (x: number) =>
|
||||
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||
const scaleHeightToCanvas = (h: number) =>
|
||||
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||
const scaleYToCanvas = (y: number) =>
|
||||
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||
ctx.strokeStyle =
|
||||
userSignatureZones.value === null ? unselectedBlue : selectedBlue;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineJoin = "bevel";
|
||||
ctx.strokeRect(
|
||||
scaleXToCanvas(zone.x),
|
||||
scaleYToCanvas(zone.y),
|
||||
scaleXToCanvas(zone.width),
|
||||
scaleHeightToCanvas(zone.height)
|
||||
);
|
||||
ctx.font = "bold 16px serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = "black";
|
||||
const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2;
|
||||
const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2;
|
||||
if (userSignatureZones.value !== null) {
|
||||
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);
|
||||
// ctx.strokeStyle = "#c6c6c6"; // halo
|
||||
// ctx.strokeText("Choisir cette", xText, yText - 12);
|
||||
// ctx.strokeText("zone de signature", xText, yText + 12);
|
||||
}
|
||||
};
|
||||
|
||||
const addZones = (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) => drawZone(z, ctx, canvas.width, canvas.height));
|
||||
}
|
||||
};
|
||||
|
||||
const checkSignature = () => {
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||
return makeFetch("GET", url)
|
||||
.then((r) => {
|
||||
signedState.value = r as SignedState;
|
||||
if (signedState.value === "pending") {
|
||||
checkForReady();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
signedState.value = "error";
|
||||
console.log('Error while checking the signature', error);
|
||||
//TODO toast error
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
signedState.value = "error";
|
||||
stopTrySigning();
|
||||
console.log("Reached the maximum number of tentative to try signing");
|
||||
//TODO toast error
|
||||
}
|
||||
if (signedState.value === "rejected") {
|
||||
stopTrySigning();
|
||||
console.log("Signature rejected by the server");
|
||||
//TODO toast error
|
||||
}
|
||||
if (signedState.value === "canceled") {
|
||||
stopTrySigning();
|
||||
console.log("Signature canceledconsole.log('Error while posting the signature', error);");
|
||||
//TODO toast error
|
||||
}
|
||||
if (signedState.value === "pending") {
|
||||
tryForReady = tryForReady + 1;
|
||||
setTimeout(() => checkSignature(), 2000);
|
||||
} else {
|
||||
stopTrySigning();
|
||||
if (signedState.value === "signed") {
|
||||
//TODO recharger le document signé
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSign = () => {
|
||||
loading.value = true;
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
|
||||
const body = {
|
||||
storedObject: signature.storedObject,
|
||||
zone: userSignatureZones.value,
|
||||
};
|
||||
makeFetch("POST", url, body)
|
||||
.then((r) => {
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Error while posting the signature', error);
|
||||
stopTrySigning();
|
||||
//TODO toast
|
||||
});
|
||||
};
|
||||
|
||||
const undoSign = async () => {
|
||||
// const canvas = document.querySelectorAll("canvas")[0];
|
||||
// const ctx = canvas.getContext("2d");
|
||||
// if (ctx && userSignatureZones.value) {
|
||||
// //drawZone(userSignatureZones.value, ctx, canvas.width, canvas.height);
|
||||
// }
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
userSignatureZones.value = null;
|
||||
};
|
||||
|
||||
downloadAndOpen();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
div#action-buttons {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
div#turn-page {
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
margin: 0 0.4rem;
|
||||
}
|
||||
}
|
||||
div.signature-modal-body {
|
||||
height: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { createApp } from "vue";
|
||||
// @ts-ignore
|
||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||
import App from "./App.vue";
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
yes: 'Oui',
|
||||
are_you_sure: 'Êtes-vous sûr·e?',
|
||||
you_are_going_to_sign: 'Vous allez signer le document',
|
||||
signature_confirmation: 'Confirmation de la signature',
|
||||
sign: 'Signer',
|
||||
choose_another_signature: 'Choisir une autre signature',
|
||||
cancel: 'Annuler',
|
||||
cancel_signing: 'Refuser de signer',
|
||||
last_sign_zone: 'Zone de signature précédente',
|
||||
next_sign_zone: 'Zone de signature suivante',
|
||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||
loading: 'Chargement...'
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(i18n)
|
||||
.component("app", App)
|
||||
.mount("#document-signature");
|
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<title>Signature</title>
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_link_tags('chill') }}
|
||||
{{ encore_entry_link_tags('vue_document_signature') }}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
<script type="text/javascript">
|
||||
window.signature = {{ signature|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_document_signature') }}
|
||||
{% endblock %}
|
||||
|
||||
<div class="content" id="content">
|
||||
<div class="container-xxl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
||||
<h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
|
||||
<div class="row" id="document-signature"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -12,9 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
@ -29,6 +32,8 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function __invoke(PdfSignedMessage $message): void
|
||||
@ -48,5 +53,9 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
}
|
||||
|
||||
$this->storedObjectManager->write($storedObject, $message->content);
|
||||
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
final readonly class PDFSignatureZone
|
||||
{
|
||||
public function __construct(
|
||||
#[Groups(['read'])]
|
||||
public int $index,
|
||||
#[Groups(['read'])]
|
||||
public float $x,
|
||||
#[Groups(['read'])]
|
||||
@ -31,7 +33,8 @@ final readonly class PDFSignatureZone
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return
|
||||
$this->x == $other->x
|
||||
$this->index == $other->index
|
||||
&& $this->x == $other->x
|
||||
&& $this->y == $other->y
|
||||
&& $this->height == $other->height
|
||||
&& $this->width == $other->width
|
||||
|
@ -20,7 +20,7 @@ class PDFSignatureZoneParser
|
||||
private Parser $parser;
|
||||
|
||||
public function __construct(
|
||||
public float $defaultHeight = 180.0,
|
||||
public float $defaultHeight = 90.0,
|
||||
public float $defaultWidth = 180.0,
|
||||
) {
|
||||
$this->parser = new Parser();
|
||||
@ -37,6 +37,7 @@ class PDFSignatureZoneParser
|
||||
$defaults = $pdf->getObjectsByType('Pages');
|
||||
$defaultPage = reset($defaults);
|
||||
$defaultPageDetails = $defaultPage->getDetails();
|
||||
$zoneIndex = 0;
|
||||
|
||||
foreach ($pdf->getPages() as $index => $page) {
|
||||
$details = $page->getDetails();
|
||||
@ -48,7 +49,8 @@ class PDFSignatureZoneParser
|
||||
|
||||
foreach ($page->getDataTm() as $dataTm) {
|
||||
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
|
||||
$zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
||||
$zones[] = new PDFSignatureZone($zoneIndex, (float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
||||
++$zoneIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -48,12 +50,16 @@ class PdfSignedMessageHandlerTest extends TestCase
|
||||
new NullLogger(),
|
||||
$this->buildEntityWorkflowManager($storedObject),
|
||||
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
||||
$this->buildSignatureRepository($signature)
|
||||
$this->buildSignatureRepository($signature),
|
||||
$this->buildEntityManager(true),
|
||||
new MockClock('now'),
|
||||
);
|
||||
|
||||
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
||||
// with the content "1234"
|
||||
$handler(new PdfSignedMessage(10, $expectedContent));
|
||||
|
||||
self::assertEquals('signed', $signature->getState()->value);
|
||||
}
|
||||
|
||||
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
||||
@ -81,4 +87,13 @@ class PdfSignedMessageHandlerTest extends TestCase
|
||||
|
||||
return $storedObjectManager;
|
||||
}
|
||||
|
||||
private function buildEntityManager(bool $willFlush): EntityManagerInterface
|
||||
{
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$em->expects($willFlush ? $this->once() : $this->never())->method('flush');
|
||||
$em->expects($willFlush ? $this->once() : $this->never())->method('clear');
|
||||
|
||||
return $em;
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
|
||||
$envelope = new Envelope(
|
||||
$request = new RequestPdfSignMessage(
|
||||
0,
|
||||
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||
0,
|
||||
'metadata to add to the signature',
|
||||
'Mme Caroline Diallo',
|
||||
@ -66,7 +66,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
|
||||
|
||||
$request = new RequestPdfSignMessage(
|
||||
0,
|
||||
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||
0,
|
||||
'metadata to add to the signature',
|
||||
'Mme Caroline Diallo',
|
||||
@ -121,7 +121,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
|
||||
$denormalizer = new class () implements DenormalizerInterface {
|
||||
public function denormalize($data, string $type, ?string $format = null, array $context = [])
|
||||
{
|
||||
return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
|
||||
return new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
|
||||
}
|
||||
|
||||
public function supportsDenormalization($data, string $type, ?string $format = null)
|
||||
|
@ -58,16 +58,18 @@ class PDFSignatureZoneParserTest extends TestCase
|
||||
__DIR__.'/data/signature_2_signature_page_1.pdf',
|
||||
[
|
||||
new PDFSignatureZone(
|
||||
0,
|
||||
127.7,
|
||||
95.289,
|
||||
180.0,
|
||||
90.0,
|
||||
180.0,
|
||||
$page = new PDFPage(0, 595.30393, 841.8897)
|
||||
),
|
||||
new PDFSignatureZone(
|
||||
1,
|
||||
269.5,
|
||||
95.289,
|
||||
180.0,
|
||||
90.0,
|
||||
180.0,
|
||||
$page,
|
||||
),
|
||||
|
@ -5,4 +5,5 @@ module.exports = function(encore)
|
||||
});
|
||||
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
|
||||
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
||||
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
|
||||
};
|
||||
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
@ -32,6 +34,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
@ -44,6 +47,7 @@ class WorkflowController extends AbstractController
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly Registry $registry,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@ -51,6 +55,7 @@ class WorkflowController extends AbstractController
|
||||
private readonly ChillSecurity $security,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
||||
@ -377,7 +382,7 @@ class WorkflowController extends AbstractController
|
||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
||||
|
||||
if ($signature->getSigner() instanceof User) {
|
||||
return $this->redirectToRoute('signature_route_user');
|
||||
return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
|
||||
}
|
||||
|
||||
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
||||
@ -401,8 +406,7 @@ class WorkflowController extends AbstractController
|
||||
$this->entityManager->persist($signature);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Todo should redirect to document for actual signing? To be adjusted still
|
||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
|
||||
return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
@ -413,4 +417,36 @@ class WorkflowController extends AbstractController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')]
|
||||
public function addSignature(int $signature_id, Request $request): Response
|
||||
{
|
||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
if (null === $storedObject) {
|
||||
throw new NotFoundHttpException('No stored object found');
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
||||
if (null != $content) {
|
||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||
}
|
||||
|
||||
$signatureClient = [];
|
||||
$signatureClient['id'] = $signature->getId();
|
||||
$signatureClient['storedObject'] = [
|
||||
'filename' => $storedObject->getFilename(),
|
||||
'iv' => $storedObject->getIv(),
|
||||
'keyInfos' => $storedObject->getKeyInfos(),
|
||||
];
|
||||
$signatureClient['zones'] = $zones;
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
||||
['signature' => $signatureClient]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
<h2>{{ 'workflow.signature_zone.title'|trans }}</h2>
|
||||
|
||||
<div class="flex-table justify-content-center">
|
||||
<div class="item-bloc">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
{% for s in signatures %}
|
||||
<div class="item-row mb-2">
|
||||
<div class="col-sm-6"><span>{{ s.signer|chill_entity_render_box }}</span></div>
|
||||
<div class="col-sm-6">
|
||||
<a class="btn btn-show" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}">{{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||
{% if s.state is same as('signed') %}
|
||||
<p class="updatedBy">{{ s.stateDate }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-8"><span>{{ s.signer|chill_entity_render_box }}</span></div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||
{% if s.state is same as('signed') %}
|
||||
<p class="updatedBy">{{ s.stateDate }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<title>Signature</title>
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_link_tags('chill') }}
|
||||
{{ encore_entry_link_tags('vue_document_signature') }}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
<script type="text/javascript">
|
||||
window.signature = {{ signature|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_document_signature') }}
|
||||
{% endblock %}
|
||||
|
||||
<div class="content" id="content">
|
||||
<div class="container-xxl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
||||
<div class="row" id="document-signature"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user