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",
|
"marked": "^12.0.2",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
|
"pdfjs-dist": "^4.3.136",
|
||||||
"swagger-ui": "^4.15.5",
|
"swagger-ui": "^4.15.5",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
|
@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class DocumentPersonController.
|
* Class DocumentPersonController.
|
||||||
@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController
|
|||||||
protected TranslatorInterface $translator,
|
protected TranslatorInterface $translator,
|
||||||
protected EventDispatcherInterface $eventDispatcher,
|
protected EventDispatcherInterface $eventDispatcher,
|
||||||
protected AuthorizationHelper $authorizationHelper,
|
protected AuthorizationHelper $authorizationHelper,
|
||||||
|
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||||
|
protected StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -197,4 +201,36 @@ class DocumentPersonController extends AbstractController
|
|||||||
['document' => $document, 'person' => $person]
|
['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;
|
namespace Chill\DocStoreBundle\Controller;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
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\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
@ -25,22 +27,41 @@ class SignatureRequestController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private MessageBusInterface $messageBus,
|
private MessageBusInterface $messageBus,
|
||||||
private StoredObjectManagerInterface $storedObjectManager,
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
private EntityWorkflowManager $entityWorkflowManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
#[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);
|
$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(
|
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
||||||
0,
|
$signature->getId(),
|
||||||
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
$zone,
|
||||||
0,
|
$data['zone']['index'],
|
||||||
'test signature',
|
'test signature', // reason (string)
|
||||||
'Mme Caroline Diallo',
|
'Mme Caroline Diallo', // signerText (string)
|
||||||
$content
|
$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, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,3 +62,24 @@ export interface PostStoreObjectSignature {
|
|||||||
signature: 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;
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||||
@ -29,6 +32,8 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
|||||||
private EntityWorkflowManager $entityWorkflowManager,
|
private EntityWorkflowManager $entityWorkflowManager,
|
||||||
private StoredObjectManagerInterface $storedObjectManager,
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private ClockInterface $clock,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(PdfSignedMessage $message): void
|
public function __invoke(PdfSignedMessage $message): void
|
||||||
@ -48,5 +53,9 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->storedObjectManager->write($storedObject, $message->content);
|
$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
|
final readonly class PDFSignatureZone
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
#[Groups(['read'])]
|
||||||
|
public int $index,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
public float $x,
|
public float $x,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
@ -31,7 +33,8 @@ final readonly class PDFSignatureZone
|
|||||||
public function equals(self $other): bool
|
public function equals(self $other): bool
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
$this->x == $other->x
|
$this->index == $other->index
|
||||||
|
&& $this->x == $other->x
|
||||||
&& $this->y == $other->y
|
&& $this->y == $other->y
|
||||||
&& $this->height == $other->height
|
&& $this->height == $other->height
|
||||||
&& $this->width == $other->width
|
&& $this->width == $other->width
|
||||||
|
@ -20,7 +20,7 @@ class PDFSignatureZoneParser
|
|||||||
private Parser $parser;
|
private Parser $parser;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public float $defaultHeight = 180.0,
|
public float $defaultHeight = 90.0,
|
||||||
public float $defaultWidth = 180.0,
|
public float $defaultWidth = 180.0,
|
||||||
) {
|
) {
|
||||||
$this->parser = new Parser();
|
$this->parser = new Parser();
|
||||||
@ -37,6 +37,7 @@ class PDFSignatureZoneParser
|
|||||||
$defaults = $pdf->getObjectsByType('Pages');
|
$defaults = $pdf->getObjectsByType('Pages');
|
||||||
$defaultPage = reset($defaults);
|
$defaultPage = reset($defaults);
|
||||||
$defaultPageDetails = $defaultPage->getDetails();
|
$defaultPageDetails = $defaultPage->getDetails();
|
||||||
|
$zoneIndex = 0;
|
||||||
|
|
||||||
foreach ($pdf->getPages() as $index => $page) {
|
foreach ($pdf->getPages() as $index => $page) {
|
||||||
$details = $page->getDetails();
|
$details = $page->getDetails();
|
||||||
@ -48,7 +49,8 @@ class PDFSignatureZoneParser
|
|||||||
|
|
||||||
foreach ($page->getDataTm() as $dataTm) {
|
foreach ($page->getDataTm() as $dataTm) {
|
||||||
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
|
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\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -48,12 +50,16 @@ class PdfSignedMessageHandlerTest extends TestCase
|
|||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->buildEntityWorkflowManager($storedObject),
|
$this->buildEntityWorkflowManager($storedObject),
|
||||||
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
$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
|
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
||||||
// with the content "1234"
|
// with the content "1234"
|
||||||
$handler(new PdfSignedMessage(10, $expectedContent));
|
$handler(new PdfSignedMessage(10, $expectedContent));
|
||||||
|
|
||||||
|
self::assertEquals('signed', $signature->getState()->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
||||||
@ -81,4 +87,13 @@ class PdfSignedMessageHandlerTest extends TestCase
|
|||||||
|
|
||||||
return $storedObjectManager;
|
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(
|
$envelope = new Envelope(
|
||||||
$request = new RequestPdfSignMessage(
|
$request = new RequestPdfSignMessage(
|
||||||
0,
|
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,
|
0,
|
||||||
'metadata to add to the signature',
|
'metadata to add to the signature',
|
||||||
'Mme Caroline Diallo',
|
'Mme Caroline Diallo',
|
||||||
@ -66,7 +66,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
|
|||||||
|
|
||||||
$request = new RequestPdfSignMessage(
|
$request = new RequestPdfSignMessage(
|
||||||
0,
|
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,
|
0,
|
||||||
'metadata to add to the signature',
|
'metadata to add to the signature',
|
||||||
'Mme Caroline Diallo',
|
'Mme Caroline Diallo',
|
||||||
@ -121,7 +121,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
|
|||||||
$denormalizer = new class () implements DenormalizerInterface {
|
$denormalizer = new class () implements DenormalizerInterface {
|
||||||
public function denormalize($data, string $type, ?string $format = null, array $context = [])
|
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)
|
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',
|
__DIR__.'/data/signature_2_signature_page_1.pdf',
|
||||||
[
|
[
|
||||||
new PDFSignatureZone(
|
new PDFSignatureZone(
|
||||||
|
0,
|
||||||
127.7,
|
127.7,
|
||||||
95.289,
|
95.289,
|
||||||
180.0,
|
90.0,
|
||||||
180.0,
|
180.0,
|
||||||
$page = new PDFPage(0, 595.30393, 841.8897)
|
$page = new PDFPage(0, 595.30393, 841.8897)
|
||||||
),
|
),
|
||||||
new PDFSignatureZone(
|
new PDFSignatureZone(
|
||||||
|
1,
|
||||||
269.5,
|
269.5,
|
||||||
95.289,
|
95.289,
|
||||||
180.0,
|
90.0,
|
||||||
180.0,
|
180.0,
|
||||||
$page,
|
$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_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('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;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||||
@ -32,6 +34,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
@ -44,6 +47,7 @@ class WorkflowController extends AbstractController
|
|||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||||
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
||||||
private readonly ValidatorInterface $validator,
|
private readonly ValidatorInterface $validator,
|
||||||
|
private readonly StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||||
private readonly PaginatorFactory $paginatorFactory,
|
private readonly PaginatorFactory $paginatorFactory,
|
||||||
private readonly Registry $registry,
|
private readonly Registry $registry,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
@ -51,6 +55,7 @@ class WorkflowController extends AbstractController
|
|||||||
private readonly ChillSecurity $security,
|
private readonly ChillSecurity $security,
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||||
private readonly ClockInterface $clock,
|
private readonly ClockInterface $clock,
|
||||||
|
private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
#[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);
|
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
||||||
|
|
||||||
if ($signature->getSigner() instanceof User) {
|
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);
|
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
||||||
@ -401,8 +406,7 @@ class WorkflowController extends AbstractController
|
|||||||
$this->entityManager->persist($signature);
|
$this->entityManager->persist($signature);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
// Todo should redirect to document for actual signing? To be adjusted still
|
return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
|
||||||
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(
|
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>
|
<h2>{{ 'workflow.signature_zone.title'|trans }}</h2>
|
||||||
|
|
||||||
<div class="flex-table justify-content-center">
|
<div class="container">
|
||||||
<div class="item-bloc">
|
<div class="row align-items-center">
|
||||||
{% for s in signatures %}
|
{% for s in signatures %}
|
||||||
<div class="item-row mb-2">
|
<div class="col-sm-12 col-md-8"><span>{{ s.signer|chill_entity_render_box }}</span></div>
|
||||||
<div class="col-sm-6"><span>{{ s.signer|chill_entity_render_box }}</span></div>
|
<div class="col-sm-12 col-md-4">
|
||||||
<div class="col-sm-6">
|
<ul class="record_actions">
|
||||||
<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>
|
<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') %}
|
{% if s.state is same as('signed') %}
|
||||||
<p class="updatedBy">{{ s.stateDate }}</p>
|
<p class="updatedBy">{{ s.stateDate }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</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