diff --git a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php index 70d126f78..26fc1b098 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php @@ -15,12 +15,17 @@ use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessa use Chill\DocStoreBundle\Service\Signature\PDFPage; use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; +use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class SignatureRequestController { @@ -28,12 +33,20 @@ class SignatureRequestController private readonly MessageBusInterface $messageBus, private readonly StoredObjectManagerInterface $storedObjectManager, private readonly EntityWorkflowManager $entityWorkflowManager, + private readonly ChillEntityRenderManagerInterface $entityRender, + private readonly NormalizerInterface $normalizer, + private readonly Security $security, ) {} #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')] public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse { $entityWorkflow = $signature->getStep()->getEntityWorkflow(); + + if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) { + return new JsonResponse([], status: Response::HTTP_CONFLICT); + } + $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow); $content = $this->storedObjectManager->read($storedObject); @@ -51,8 +64,14 @@ class SignatureRequestController $signature->getId(), $zone, $data['zone']['index'], - 'test signature', // reason (string) - 'Mme Caroline Diallo', // signerText (string) + 'Signed by IP: '.(string) $request->getClientIp().', authenticated user: '.$this->entityRender->renderString($this->security->getUser(), []), + $this->entityRender->renderString($signature->getSigner(), [ + // options for user render + 'absence' => false, + 'main_scope' => false, + // options for person render + 'addAge' => false, + ]), $content )); @@ -62,6 +81,16 @@ class SignatureRequestController #[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, []); + $entityWorkflow = $signature->getStep()->getEntityWorkflow(); + $storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow); + + return new JsonResponse( + [ + 'state' => $signature->getState(), + 'storedObject' => $this->normalizer->normalize($storedObject, 'json'), + ], + JsonResponse::HTTP_OK, + [] + ); } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 840421991..ee6b2bf98 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -1,100 +1,119 @@ -import {DateTime, User} from "../../../ChillMainBundle/Resources/public/types"; +import { + DateTime, + User, +} from "../../../ChillMainBundle/Resources/public/types"; -export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending"; +export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending"; export interface StoredObject { - id: number, - title: string|null, - uuid: string, - prefix: string, - status: StoredObjectStatus, - currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted, - totalVersions: number, - datas: object, + id: number; + title: string | null; + uuid: string; + prefix: string; + status: StoredObjectStatus; + currentVersion: + | null + | StoredObjectVersionCreated + | StoredObjectVersionPersisted; + totalVersions: number; + datas: object; /** @deprecated */ - creationDate: DateTime, - createdAt: DateTime|null, - createdBy: User|null, + creationDate: DateTime; + createdAt: DateTime | null; + createdBy: User | null; _permissions: { - canEdit: boolean, - canSee: boolean, - }, + canEdit: boolean; + canSee: boolean; + }; _links?: { dav_link?: { - href: string - expiration: number - }, - }, + href: string; + expiration: number; + }; + }; } export interface StoredObjectVersion { /** * filename of the object in the object storage */ - filename: string, - iv: number[], - keyInfos: JsonWebKey, - type: string, + filename: string; + iv: number[]; + keyInfos: JsonWebKey; + type: string; } export interface StoredObjectVersionCreated extends StoredObjectVersion { - persisted: false, + persisted: false; } -export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated { - version: number, - id: number, - createdAt: DateTime|null, - createdBy: User|null, +export interface StoredObjectVersionPersisted + extends StoredObjectVersionCreated { + version: number; + id: number; + createdAt: DateTime | null; + createdBy: User | null; } export interface StoredObjectStatusChange { - id: number, - filename: string, - status: StoredObjectStatus, - type: string, + id: number; + filename: string; + status: StoredObjectStatus; + type: string; } /** * Function executed by the WopiEditButton component. */ export type WopiEditButtonExecutableBeforeLeaveFunction = { - (): Promise -} + (): Promise; +}; /** * 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, + index: number; + width: number; + height: number; } export interface SignatureZone { - index: number, - x: number, - y: number, - width: number, - height: number, - PDFPage: PDFPage, + index: number | null; + x: number; + y: number; + width: number; + height: number; + PDFPage: PDFPage; } export interface Signature { - id: number, - storedObject: StoredObject, - zones: SignatureZone[], + id: number; + storedObject: StoredObject; + zones: SignatureZone[]; } -export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error'; +export type SignedState = + | "pending" + | "signed" + | "rejected" + | "canceled" + | "error"; + +export interface CheckSignature { + state: SignedState; + storedObject: StoredObject; +} + +export type CanvasEvent = "select" | "add"; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 47edd87c3..e63997bcd 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -26,37 +26,9 @@ -
-
-
- -
-
- -
-
- -
-
+
+
+
- page {{ page }} / {{ pageCount }} + {{ page }}/{{ pageCount }}
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + {{ page }} / {{ pageCount }} + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
-
+
-
+
@@ -119,7 +229,13 @@ import { ref, Ref, reactive } from "vue"; import { useToast } from "vue-toast-notification"; import "vue-toast-notification/dist/theme-sugar.css"; -import { Signature, SignatureZone, SignedState } from "../../types"; +import { + CanvasEvent, + CheckSignature, + Signature, + SignatureZone, + SignedState, +} from "../../types"; import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import * as pdfjsLib from "pdfjs-dist"; import { @@ -135,19 +251,18 @@ console.log(PdfWorker); // incredible but this is needed // pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; -import { - download_and_decrypt_doc, -} from "../StoredObjectButton/helpers"; +import { download_and_decrypt_doc } from "../StoredObjectButton/helpers"; pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs"; const modalOpen: Ref = ref(false); const loading: Ref = ref(false); +const adding: Ref = ref(false); +const canvasEvent: Ref = ref("select"); const signedState: Ref = ref("pending"); const page: Ref = ref(1); const pageCount: Ref = ref(0); let userSignatureZone: Ref = ref(null); -let pdfSource: Ref = ref(""); let pdf = {} as PDFDocumentProxy; declare global { @@ -160,11 +275,13 @@ const $toast = useToast(); const signature = window.signature; +console.log(signature); + const mountPdf = async (url: string) => { const loadingTask = pdfjsLib.getDocument(url); pdf = await loadingTask.promise; pageCount.value = pdf.numPages; - await setPage(1); + await setPage(page.value); }; const getRenderContext = (pdfPage: PDFPageProxy) => { @@ -187,59 +304,61 @@ const setPage = async (page: number) => { await pdfPage.render(renderContext); }; +const init = () => downloadAndOpen().then(initPdf); + async function downloadAndOpen(): Promise { let raw; try { - raw = await download_and_decrypt_doc(signature.storedObject, signature.storedObject.currentVersion); + raw = await download_and_decrypt_doc( + signature.storedObject, + signature.storedObject.currentVersion + ); } catch (e) { console.error("error while downloading and decrypting document", e); throw e; } await mountPdf(URL.createObjectURL(raw)); - initPdf(); return raw; } const initPdf = () => { const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement; - canvas.addEventListener( - "pointerup", - (e: PointerEvent) => canvasClick(e, canvas), - false - ); - setTimeout(() => addZones(page.value), 800); + canvas.addEventListener("pointerup", canvasClick, false); + setTimeout(() => drawAllZones(page.value), 800); }; +const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) => + Math.round((x * canvasWidth) / PDFWidth); + +const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) => + Math.round((h * canvasHeight) / PDFHeight); + const hitSignature = ( zone: SignatureZone, xy: number[], canvasWidth: number, canvasHeight: number -) => { - 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) - ); -}; +) => + scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] && + xy[0] < + scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) && + zone.PDFPage.height - + scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) < + xy[1] && + xy[1] < + scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) + + zone.PDFPage.height; const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => { userSignatureZone.value = z; const ctx = canvas.getContext("2d"); if (ctx) { setPage(page.value); - setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200); + setTimeout(() => drawAllZones(page.value), 200); } }; -const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) => +const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) => signature.zones .filter((z) => z.PDFPage.index + 1 === page.value) .map((z) => { @@ -256,11 +375,18 @@ const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) => } }); +const canvasClick = (e: PointerEvent) => { + const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement; + canvasEvent.value === "select" + ? selectZoneEvent(e, canvas) + : addZoneEvent(e, canvas); +}; + const turnPage = async (upOrDown: number) => { - userSignatureZone.value = null; + //userSignatureZone.value = null; // desactivate the reset of the zone when turning page page.value = page.value + upOrDown; await setPage(page.value); - setTimeout(() => addZones(page.value), 200); + setTimeout(() => drawAllZones(page.value), 200); }; const turnSignature = async (upOrDown: number) => { @@ -290,12 +416,6 @@ const drawZone = ( ) => { 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 = userSignatureZone.value?.index === zone.index ? selectedBlue @@ -303,16 +423,22 @@ const drawZone = ( ctx.lineWidth = 2; ctx.lineJoin = "bevel"; ctx.strokeRect( - scaleXToCanvas(zone.x), - scaleYToCanvas(zone.y), - scaleXToCanvas(zone.width), - scaleHeightToCanvas(zone.height) + scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width), + zone.PDFPage.height - + scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height), + scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width), + scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) ); ctx.font = "bold 16px serif"; ctx.textAlign = "center"; ctx.fillStyle = "black"; - const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2; - const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2; + const xText = + scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) + + scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2; + const yText = + zone.PDFPage.height - + scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) + + scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2; if (userSignatureZone.value?.index === zone.index) { ctx.fillStyle = selectedBlue; ctx.fillText("Signer ici", xText, yText); @@ -320,27 +446,33 @@ const drawZone = ( 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 drawAllZones = (page: number) => { const canvas = document.querySelectorAll("canvas")[0]; const ctx = canvas.getContext("2d"); - if (ctx) { + if (ctx && signedState.value !== "signed") { signature.zones .filter((z) => z.PDFPage.index + 1 === page) - .map((z) => drawZone(z, ctx, canvas.width, canvas.height)); + .map((z) => { + if (userSignatureZone.value) { + if (userSignatureZone.value?.index === z.index) { + drawZone(z, ctx, canvas.width, canvas.height); + } + } else { + drawZone(z, ctx, canvas.width, canvas.height); + } + }); } }; const checkSignature = () => { const url = `/api/1.0/document/workflow/${signature.id}/check-signature`; - return makeFetch("GET", url) + return makeFetch("GET", url) .then((r) => { - signedState.value = r as SignedState; + signedState.value = r.state; + signature.storedObject = r.storedObject; checkForReady(); }) .catch((error) => { @@ -414,22 +546,66 @@ const confirmSign = () => { }; const undoSign = async () => { - // const canvas = document.querySelectorAll("canvas")[0]; - // const ctx = canvas.getContext("2d"); - // if (ctx && userSignatureZone.value) { - // //drawZone(userSignatureZone.value, ctx, canvas.width, canvas.height); - // } + signature.zones = signature.zones.filter((z) => z.index !== null); await setPage(page.value); - setTimeout(() => addZones(page.value), 200); + setTimeout(() => drawAllZones(page.value), 200); userSignatureZone.value = null; + adding.value = false; + canvasEvent.value = "select"; }; -downloadAndOpen(); +const toggleAddZone = () => { + canvasEvent.value === "select" + ? (canvasEvent.value = "add") + : (canvasEvent.value = "select"); +}; + +const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => { + const BOX_WIDTH = 180; + const BOX_HEIGHT = 90; + const PDFPageHeight = canvas.height; + const PDFPageWidth = canvas.width; + + const x = e.offsetX; + const y = e.offsetY; + const newZone: SignatureZone = { + index: null, + x: + scaleXToCanvas(x, canvas.width, PDFPageWidth) - + scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth), + y: + PDFPageHeight - + scaleYToCanvas(y, canvas.height, PDFPageHeight) + + scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight), + width: BOX_WIDTH, + height: BOX_HEIGHT, + PDFPage: { + index: page.value - 1, + width: PDFPageWidth, + height: PDFPageHeight, + }, + }; + signature.zones.push(newZone); + userSignatureZone.value = newZone; + + await setPage(page.value); + setTimeout(() => drawAllZones(page.value), 200); + canvasEvent.value = "select"; + adding.value = true; +}; + +const getReturnPath = () => + window.location.search + ? window.location.search.split("?returnPath=")[1] ?? + window.location.pathname + : window.location.pathname; + +init();