Merge branch 'signature-app/add-manual-zone' into 'signature-app-master'

Improve signature app

See merge request Chill-Projet/chill-bundles!725
This commit is contained in:
Julien Fastré 2024-09-13 14:23:44 +00:00
commit 1494c7ecd7
16 changed files with 538 additions and 236 deletions

View File

@ -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,
[]
);
}
}

View File

@ -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<void>
}
(): Promise<void>;
};
/**
* 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";

View File

@ -26,37 +26,9 @@
</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="userSignatureZone === null || userSignatureZone?.index < 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
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
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">
<div class="col-12 m-auto">
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
<div v-if="pageCount > 1" class="col text-center turn-page">
<button
class="btn btn-light btn-sm"
:disabled="page <= 1"
@ -64,7 +36,7 @@
>
</button>
<span>page {{ page }} / {{ pageCount }}</span>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-sm"
:disabled="page >= pageCount"
@ -73,15 +45,161 @@
</button>
</div>
<div v-if="signature.zones.length > 1" class="col-3 p-0">
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_zone") }}
</button>
</div>
<div v-if="signature.zones.length > 1" class="col-3 p-0">
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ $t("next_zone") }}
</button>
</div>
<div class="col text-end p-0">
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@click="undoSign"
v-if="signature.zones.length > 1"
:title="$t('choose_another_signature')"
>
{{ $t("another_zone") }}
</button>
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@click="undoSign"
v-else
>
{{ $t("cancel") }}
</button>
</div>
<div class="col-1" v-if="signedState !== 'signed'">
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
></button>
</div>
</div>
<div class="col-12 text-center">
<div
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
>
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
<button
class="btn btn-light btn-sm"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }} / {{ pageCount }}</span>
<button
class="btn btn-light btn-sm"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-end d-xl-none"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_zone") }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-start d-xl-none"
>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ $t("next_zone") }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-end d-none d-xl-flex p-0"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_sign_zone") }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-start d-none d-xl-flex p-0"
>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ $t("next_sign_zone") }}
</button>
</div>
<div class="col text-end p-0" v-if="signedState !== 'signed'">
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@click="undoSign"
v-if="signature.zones.length > 1"
>
{{ $t("choose_another_signature") }}
</button>
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@click="undoSign"
v-else
>
{{ $t("cancel") }}
</button>
</div>
<div
class="col text-end p-0 pe-2 pe-xxl-4"
v-if="signedState !== 'signed'"
>
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
>
{{ $t("add_zone") }}
</button>
</div>
</div>
</div>
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
<canvas class="m-auto" id="canvas"></canvas>
</div>
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
<div class="row">
<div class="col-6">
<div class="col-4" v-if="signedState !== 'signed'">
<button
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@ -90,26 +208,18 @@
{{ $t("sign") }}
</button>
</div>
<div class="col-6 d-flex justify-content-end">
<button
class="btn btn-misc me-2"
:hidden="!userSignatureZone"
@click="undoSign"
v-if="signature.zones.length > 1"
<div class="col-4" v-else></div>
<div class="col-8 d-flex justify-content-end">
<a
class="btn btn-delete"
v-if="signedState !== 'signed'"
:href="getReturnPath()"
>
{{ $t("choose_another_signature") }}
</button>
<button
class="btn btn-misc me-2"
:hidden="!userSignatureZone"
@click="undoSign"
v-else
>
{{ $t("cancel") }}
</button>
<button class="btn btn-delete" @click="undoSign">
{{ $t("cancel_signing") }}
</button>
</a>
<a class="btn btn-misc" v-else :href="getReturnPath()">
{{ $t("return") }}
</a>
</div>
</div>
</div>
@ -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<boolean> = ref(false);
const loading: Ref<boolean> = ref(false);
const adding: Ref<boolean> = ref(false);
const canvasEvent: Ref<CanvasEvent> = ref("select");
const signedState: Ref<SignedState> = ref("pending");
const page: Ref<number> = ref(1);
const pageCount: Ref<number> = ref(0);
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
let pdfSource: Ref<string> = 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<Blob> {
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<null, CheckSignature>("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();
</script>
<style scoped lang="scss">
#canvas {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
}
div#action-buttons {
position: sticky;
@ -437,7 +613,15 @@ div#action-buttons {
background-color: white;
z-index: 100;
}
div#turn-page {
div.pdf-tools {
background-color: #f3f3f3;
font-size: 0.8rem;
@media (min-width: 1400px) {
// background: none;
// border: none !important;
}
}
div.turn-page {
span {
font-size: 0.8rem;
margin: 0 0.4rem;

View File

@ -10,13 +10,20 @@ const appMessages = {
you_are_going_to_sign: 'Vous allez signer le document',
signature_confirmation: 'Confirmation de la signature',
sign: 'Signer',
choose_another_signature: 'Choisir une autre zone de signature',
choose_another_signature: 'Choisir une autre zone',
cancel: 'Annuler',
cancel_signing: 'Refuser de signer',
last_sign_zone: 'Zone de signature précédente',
next_sign_zone: 'Zone de signature suivante',
add_sign_zone: 'Ajouter une zone de signature',
last_zone: 'Zone précédente',
next_zone: 'Zone suivante',
add_zone: 'Ajouter une zone',
another_zone: 'Autre zone',
electronic_signature_in_progress: 'Signature électronique en cours...',
loading: 'Chargement...'
loading: 'Chargement...',
remove_sign_zone: 'Enlever la zone',
return: 'Retour',
}
}

View File

@ -18,7 +18,7 @@ final readonly class PdfSignedMessage
{
public function __construct(
public readonly int $signatureId,
public readonly int $signatureZoneIndex,
public readonly ?int $signatureZoneIndex,
public readonly string $content,
) {}
}

View File

@ -21,7 +21,7 @@ final readonly class RequestPdfSignMessage
public function __construct(
public int $signatureId,
public PDFSignatureZone $PDFSignatureZone,
public int $signatureZoneIndex,
public ?int $signatureZoneIndex,
public string $reason,
public string $signerText,
public string $content,

View File

@ -17,7 +17,7 @@ final readonly class PDFSignatureZone
{
public function __construct(
#[Groups(['read'])]
public int $index,
public ?int $index,
#[Groups(['read'])]
public float $x,
#[Groups(['read'])]

View File

@ -12,12 +12,15 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment;
@ -28,6 +31,7 @@ final readonly class WorkflowAddSignatureController
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
private NormalizerInterface $normalizer,
private Environment $twig,
private UrlGeneratorInterface $urlGenerator,
) {}
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
@ -35,6 +39,16 @@ final readonly class WorkflowAddSignatureController
{
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
);
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
throw new NotFoundHttpException('No stored object found');

View File

@ -396,7 +396,10 @@ class WorkflowController extends AbstractController
}
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
@ -420,7 +423,10 @@ class WorkflowController extends AbstractController
$this->entityManager->persist($signature);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
return $this->render(

View File

@ -25,13 +25,11 @@
{% 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="col-12 m-auto">
<div class="row" id="document-signature"></div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -19,24 +19,7 @@ use Twig\TwigFilter;
*/
class ChillEntityRenderExtension extends AbstractExtension
{
/**
* @var ChillEntityRender
*/
protected $defaultRender;
/**
* @var iterable|ChillEntityRenderInterface[]
*/
protected $renders = [];
/**
* ChillEntityRenderExtension constructor.
*/
public function __construct(iterable $renders)
{
$this->defaultRender = new ChillEntityRender();
$this->renders = $renders;
}
public function __construct(private readonly ChillEntityRenderManagerInterface $renderManager) {}
/**
* @return array|TwigFilter[]
@ -53,34 +36,13 @@ class ChillEntityRenderExtension extends AbstractExtension
];
}
public function renderBox($entity, array $options = []): string
public function renderBox(?object $entity, array $options = []): string
{
if (null === $entity) {
return '';
return $this->renderManager->renderBox($entity, $options);
}
return $this->getRender($entity, $options)
->renderBox($entity, $options);
}
public function renderString($entity, array $options = []): string
public function renderString(?object $entity, array $options = []): string
{
if (null === $entity) {
return '';
}
return $this->getRender($entity, $options)
->renderString($entity, $options);
}
protected function getRender($entity, $options): ?ChillEntityRenderInterface
{
foreach ($this->renders as $render) {
if ($render->supports($entity, $options)) {
return $render;
}
}
return $this->defaultRender;
return $this->renderManager->renderString($entity, $options);
}
}

View File

@ -15,7 +15,7 @@ namespace Chill\MainBundle\Templating\Entity;
* Interface to implement which will render an entity in template on a custom
* manner.
*
* @template T
* @template T of object
*/
interface ChillEntityRenderInterface
{
@ -31,7 +31,7 @@ interface ChillEntityRenderInterface
* </span>
* ```
*
* @param T $entity
* @param T|null $entity
*
* @phpstan-pure
*/
@ -42,7 +42,7 @@ interface ChillEntityRenderInterface
*
* Example: returning the name of a person.
*
* @param T $entity
* @param T|null $entity
*
* @phpstan-pure
*/

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Entity;
final readonly class ChillEntityRenderManager implements ChillEntityRenderManagerInterface
{
private ChillEntityRender $defaultRender;
public function __construct(/**
* @var iterable<ChillEntityRenderInterface>
*/
private iterable $renders)
{
$this->defaultRender = new ChillEntityRender();
}
public function renderBox($entity, array $options = []): string
{
if (null === $entity) {
return '';
}
return $this->getRender($entity, $options)
->renderBox($entity, $options);
}
public function renderString($entity, array $options = []): string
{
if (null === $entity) {
return '';
}
return $this->getRender($entity, $options)
->renderString($entity, $options);
}
private function getRender($entity, $options): ChillEntityRenderInterface
{
foreach ($this->renders as $render) {
if ($render->supports($entity, $options)) {
return $render;
}
}
return $this->defaultRender;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Entity;
interface ChillEntityRenderManagerInterface
{
public function renderBox(?object $entity, array $options = []): string;
public function renderString(?object $entity, array $options = []): string;
}

View File

@ -23,6 +23,7 @@ use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment;
@ -62,7 +63,9 @@ class WorkflowAddSignatureControllerTest extends TestCase
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
->willReturn('ok');
$controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig);
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig, $urlGenerator);
$actual = $controller($signature, new Request());

View File

@ -32,11 +32,16 @@ services:
- { name: twig.extension }
Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension:
arguments:
$renders: !tagged_iterator chill.render_entity
tags:
- { name: twig.extension }
Chill\MainBundle\Templating\Entity\ChillEntityRenderManager:
arguments:
$renders: !tagged_iterator chill.render_entity
Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface:
alias: 'Chill\MainBundle\Templating\Entity\ChillEntityRenderManager'
Chill\MainBundle\Templating\Entity\CommentRender:
tags:
- { name: 'chill.render_entity' }