mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
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:
commit
1494c7ecd7
@ -15,12 +15,17 @@ use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessa
|
|||||||
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 Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
class SignatureRequestController
|
class SignatureRequestController
|
||||||
{
|
{
|
||||||
@ -28,12 +33,20 @@ class SignatureRequestController
|
|||||||
private readonly MessageBusInterface $messageBus,
|
private readonly MessageBusInterface $messageBus,
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
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')]
|
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||||
|
|
||||||
|
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
||||||
|
return new JsonResponse([], status: Response::HTTP_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||||
$content = $this->storedObjectManager->read($storedObject);
|
$content = $this->storedObjectManager->read($storedObject);
|
||||||
|
|
||||||
@ -51,8 +64,14 @@ class SignatureRequestController
|
|||||||
$signature->getId(),
|
$signature->getId(),
|
||||||
$zone,
|
$zone,
|
||||||
$data['zone']['index'],
|
$data['zone']['index'],
|
||||||
'test signature', // reason (string)
|
'Signed by IP: '.(string) $request->getClientIp().', authenticated user: '.$this->entityRender->renderString($this->security->getUser(), []),
|
||||||
'Mme Caroline Diallo', // signerText (string)
|
$this->entityRender->renderString($signature->getSigner(), [
|
||||||
|
// options for user render
|
||||||
|
'absence' => false,
|
||||||
|
'main_scope' => false,
|
||||||
|
// options for person render
|
||||||
|
'addAge' => false,
|
||||||
|
]),
|
||||||
$content
|
$content
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -62,6 +81,16 @@ class SignatureRequestController
|
|||||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
||||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
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,
|
||||||
|
[]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
export interface StoredObject {
|
||||||
id: number,
|
id: number;
|
||||||
title: string|null,
|
title: string | null;
|
||||||
uuid: string,
|
uuid: string;
|
||||||
prefix: string,
|
prefix: string;
|
||||||
status: StoredObjectStatus,
|
status: StoredObjectStatus;
|
||||||
currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted,
|
currentVersion:
|
||||||
totalVersions: number,
|
| null
|
||||||
datas: object,
|
| StoredObjectVersionCreated
|
||||||
|
| StoredObjectVersionPersisted;
|
||||||
|
totalVersions: number;
|
||||||
|
datas: object;
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
creationDate: DateTime,
|
creationDate: DateTime;
|
||||||
createdAt: DateTime|null,
|
createdAt: DateTime | null;
|
||||||
createdBy: User|null,
|
createdBy: User | null;
|
||||||
_permissions: {
|
_permissions: {
|
||||||
canEdit: boolean,
|
canEdit: boolean;
|
||||||
canSee: boolean,
|
canSee: boolean;
|
||||||
},
|
};
|
||||||
_links?: {
|
_links?: {
|
||||||
dav_link?: {
|
dav_link?: {
|
||||||
href: string
|
href: string;
|
||||||
expiration: number
|
expiration: number;
|
||||||
},
|
};
|
||||||
},
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersion {
|
export interface StoredObjectVersion {
|
||||||
/**
|
/**
|
||||||
* filename of the object in the object storage
|
* filename of the object in the object storage
|
||||||
*/
|
*/
|
||||||
filename: string,
|
filename: string;
|
||||||
iv: number[],
|
iv: number[];
|
||||||
keyInfos: JsonWebKey,
|
keyInfos: JsonWebKey;
|
||||||
type: string,
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
||||||
persisted: false,
|
persisted: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
|
export interface StoredObjectVersionPersisted
|
||||||
version: number,
|
extends StoredObjectVersionCreated {
|
||||||
id: number,
|
version: number;
|
||||||
createdAt: DateTime|null,
|
id: number;
|
||||||
createdBy: User|null,
|
createdAt: DateTime | null;
|
||||||
|
createdBy: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredObjectStatusChange {
|
export interface StoredObjectStatusChange {
|
||||||
id: number,
|
id: number;
|
||||||
filename: string,
|
filename: string;
|
||||||
status: StoredObjectStatus,
|
status: StoredObjectStatus;
|
||||||
type: string,
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function executed by the WopiEditButton component.
|
* Function executed by the WopiEditButton component.
|
||||||
*/
|
*/
|
||||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||||
(): Promise<void>
|
(): Promise<void>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object containing information for performering a POST request to a swift object store
|
* Object containing information for performering a POST request to a swift object store
|
||||||
*/
|
*/
|
||||||
export interface PostStoreObjectSignature {
|
export interface PostStoreObjectSignature {
|
||||||
method: "POST",
|
method: "POST";
|
||||||
max_file_size: number,
|
max_file_size: number;
|
||||||
max_file_count: 1,
|
max_file_count: 1;
|
||||||
expires: number,
|
expires: number;
|
||||||
submit_delay: 180,
|
submit_delay: 180;
|
||||||
redirect: string,
|
redirect: string;
|
||||||
prefix: string,
|
prefix: string;
|
||||||
url: string,
|
url: string;
|
||||||
signature: string,
|
signature: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFPage {
|
export interface PDFPage {
|
||||||
index: number,
|
index: number;
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
}
|
}
|
||||||
export interface SignatureZone {
|
export interface SignatureZone {
|
||||||
index: number,
|
index: number | null;
|
||||||
x: number,
|
x: number;
|
||||||
y: number,
|
y: number;
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
PDFPage: PDFPage,
|
PDFPage: PDFPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Signature {
|
export interface Signature {
|
||||||
id: number,
|
id: number;
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject;
|
||||||
zones: SignatureZone[],
|
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";
|
||||||
|
@ -26,37 +26,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</teleport>
|
</teleport>
|
||||||
<div class="col-12">
|
<div class="col-12 m-auto">
|
||||||
<div
|
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||||
class="row justify-content-center mb-2"
|
<div v-if="pageCount > 1" class="col text-center turn-page">
|
||||||
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">
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
:disabled="page <= 1"
|
:disabled="page <= 1"
|
||||||
@ -64,7 +36,7 @@
|
|||||||
>
|
>
|
||||||
❮
|
❮
|
||||||
</button>
|
</button>
|
||||||
<span>page {{ page }} / {{ pageCount }}</span>
|
<span>{{ page }}/{{ pageCount }}</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-light btn-sm"
|
class="btn btn-light btn-sm"
|
||||||
:disabled="page >= pageCount"
|
:disabled="page >= pageCount"
|
||||||
@ -73,15 +45,161 @@
|
|||||||
❯
|
❯
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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="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>
|
</div>
|
||||||
<div class="col-12 text-center">
|
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
|
||||||
<canvas class="m-auto" id="canvas"></canvas>
|
<canvas class="m-auto" id="canvas"></canvas>
|
||||||
</div>
|
</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="row">
|
||||||
<div class="col-6">
|
<div class="col-4" v-if="signedState !== 'signed'">
|
||||||
<button
|
<button
|
||||||
class="btn btn-action me-2"
|
class="btn btn-action me-2"
|
||||||
:disabled="!userSignatureZone"
|
:disabled="!userSignatureZone"
|
||||||
@ -90,26 +208,18 @@
|
|||||||
{{ $t("sign") }}
|
{{ $t("sign") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 d-flex justify-content-end">
|
<div class="col-4" v-else></div>
|
||||||
<button
|
<div class="col-8 d-flex justify-content-end">
|
||||||
class="btn btn-misc me-2"
|
<a
|
||||||
:hidden="!userSignatureZone"
|
class="btn btn-delete"
|
||||||
@click="undoSign"
|
v-if="signedState !== 'signed'"
|
||||||
v-if="signature.zones.length > 1"
|
: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") }}
|
{{ $t("cancel_signing") }}
|
||||||
</button>
|
</a>
|
||||||
|
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||||
|
{{ $t("return") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,7 +229,13 @@
|
|||||||
import { ref, Ref, reactive } from "vue";
|
import { ref, Ref, reactive } from "vue";
|
||||||
import { useToast } from "vue-toast-notification";
|
import { useToast } from "vue-toast-notification";
|
||||||
import "vue-toast-notification/dist/theme-sugar.css";
|
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 { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from "pdfjs-dist";
|
||||||
import {
|
import {
|
||||||
@ -135,19 +251,18 @@ console.log(PdfWorker); // incredible but this is needed
|
|||||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||||
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
import {
|
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
|
||||||
download_and_decrypt_doc,
|
|
||||||
} from "../StoredObjectButton/helpers";
|
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||||
|
|
||||||
const modalOpen: Ref<boolean> = ref(false);
|
const modalOpen: Ref<boolean> = ref(false);
|
||||||
const loading: 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 signedState: Ref<SignedState> = ref("pending");
|
||||||
const page: Ref<number> = ref(1);
|
const page: Ref<number> = ref(1);
|
||||||
const pageCount: Ref<number> = ref(0);
|
const pageCount: Ref<number> = ref(0);
|
||||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||||
let pdfSource: Ref<string> = ref("");
|
|
||||||
let pdf = {} as PDFDocumentProxy;
|
let pdf = {} as PDFDocumentProxy;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -160,11 +275,13 @@ const $toast = useToast();
|
|||||||
|
|
||||||
const signature = window.signature;
|
const signature = window.signature;
|
||||||
|
|
||||||
|
console.log(signature);
|
||||||
|
|
||||||
const mountPdf = async (url: string) => {
|
const mountPdf = async (url: string) => {
|
||||||
const loadingTask = pdfjsLib.getDocument(url);
|
const loadingTask = pdfjsLib.getDocument(url);
|
||||||
pdf = await loadingTask.promise;
|
pdf = await loadingTask.promise;
|
||||||
pageCount.value = pdf.numPages;
|
pageCount.value = pdf.numPages;
|
||||||
await setPage(1);
|
await setPage(page.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||||
@ -187,59 +304,61 @@ const setPage = async (page: number) => {
|
|||||||
await pdfPage.render(renderContext);
|
await pdfPage.render(renderContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const init = () => downloadAndOpen().then(initPdf);
|
||||||
|
|
||||||
async function downloadAndOpen(): Promise<Blob> {
|
async function downloadAndOpen(): Promise<Blob> {
|
||||||
let raw;
|
let raw;
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error("error while downloading and decrypting document", e);
|
console.error("error while downloading and decrypting document", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
await mountPdf(URL.createObjectURL(raw));
|
await mountPdf(URL.createObjectURL(raw));
|
||||||
initPdf();
|
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPdf = () => {
|
const initPdf = () => {
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||||
canvas.addEventListener(
|
canvas.addEventListener("pointerup", canvasClick, false);
|
||||||
"pointerup",
|
setTimeout(() => drawAllZones(page.value), 800);
|
||||||
(e: PointerEvent) => canvasClick(e, canvas),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
setTimeout(() => addZones(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 = (
|
const hitSignature = (
|
||||||
zone: SignatureZone,
|
zone: SignatureZone,
|
||||||
xy: number[],
|
xy: number[],
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number
|
canvasHeight: number
|
||||||
) => {
|
) =>
|
||||||
const scaleXToCanvas = (x: number) =>
|
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||||||
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
xy[0] <
|
||||||
const scaleHeightToCanvas = (h: number) =>
|
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||||||
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
zone.PDFPage.height -
|
||||||
const scaleYToCanvas = (y: number) =>
|
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
||||||
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
xy[1] &&
|
||||||
return (
|
xy[1] <
|
||||||
scaleXToCanvas(zone.x) < xy[0] &&
|
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
||||||
xy[0] < scaleXToCanvas(zone.x + zone.width) &&
|
zone.PDFPage.height;
|
||||||
scaleYToCanvas(zone.y) < xy[1] &&
|
|
||||||
xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||||
userSignatureZone.value = z;
|
userSignatureZone.value = z;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
setPage(page.value);
|
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
|
signature.zones
|
||||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||||||
.map((z) => {
|
.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) => {
|
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;
|
page.value = page.value + upOrDown;
|
||||||
await setPage(page.value);
|
await setPage(page.value);
|
||||||
setTimeout(() => addZones(page.value), 200);
|
setTimeout(() => drawAllZones(page.value), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
const turnSignature = async (upOrDown: number) => {
|
const turnSignature = async (upOrDown: number) => {
|
||||||
@ -290,12 +416,6 @@ const drawZone = (
|
|||||||
) => {
|
) => {
|
||||||
const unselectedBlue = "#007bff";
|
const unselectedBlue = "#007bff";
|
||||||
const selectedBlue = "#034286";
|
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 =
|
ctx.strokeStyle =
|
||||||
userSignatureZone.value?.index === zone.index
|
userSignatureZone.value?.index === zone.index
|
||||||
? selectedBlue
|
? selectedBlue
|
||||||
@ -303,16 +423,22 @@ const drawZone = (
|
|||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.lineJoin = "bevel";
|
ctx.lineJoin = "bevel";
|
||||||
ctx.strokeRect(
|
ctx.strokeRect(
|
||||||
scaleXToCanvas(zone.x),
|
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||||||
scaleYToCanvas(zone.y),
|
zone.PDFPage.height -
|
||||||
scaleXToCanvas(zone.width),
|
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
||||||
scaleHeightToCanvas(zone.height)
|
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
||||||
|
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
||||||
);
|
);
|
||||||
ctx.font = "bold 16px serif";
|
ctx.font = "bold 16px serif";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2;
|
const xText =
|
||||||
const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2;
|
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) {
|
if (userSignatureZone.value?.index === zone.index) {
|
||||||
ctx.fillStyle = selectedBlue;
|
ctx.fillStyle = selectedBlue;
|
||||||
ctx.fillText("Signer ici", xText, yText);
|
ctx.fillText("Signer ici", xText, yText);
|
||||||
@ -320,27 +446,33 @@ const drawZone = (
|
|||||||
ctx.fillStyle = unselectedBlue;
|
ctx.fillStyle = unselectedBlue;
|
||||||
ctx.fillText("Choisir cette", xText, yText - 12);
|
ctx.fillText("Choisir cette", xText, yText - 12);
|
||||||
ctx.fillText("zone de signature", 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 canvas = document.querySelectorAll("canvas")[0];
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx) {
|
if (ctx && signedState.value !== "signed") {
|
||||||
signature.zones
|
signature.zones
|
||||||
.filter((z) => z.PDFPage.index + 1 === page)
|
.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 checkSignature = () => {
|
||||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||||
return makeFetch("GET", url)
|
return makeFetch<null, CheckSignature>("GET", url)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
signedState.value = r as SignedState;
|
signedState.value = r.state;
|
||||||
|
signature.storedObject = r.storedObject;
|
||||||
checkForReady();
|
checkForReady();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -414,22 +546,66 @@ const confirmSign = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const undoSign = async () => {
|
const undoSign = async () => {
|
||||||
// const canvas = document.querySelectorAll("canvas")[0];
|
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||||
// const ctx = canvas.getContext("2d");
|
|
||||||
// if (ctx && userSignatureZone.value) {
|
|
||||||
// //drawZone(userSignatureZone.value, ctx, canvas.width, canvas.height);
|
|
||||||
// }
|
|
||||||
await setPage(page.value);
|
await setPage(page.value);
|
||||||
setTimeout(() => addZones(page.value), 200);
|
setTimeout(() => drawAllZones(page.value), 200);
|
||||||
userSignatureZone.value = null;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
#canvas {
|
#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 {
|
div#action-buttons {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@ -437,7 +613,15 @@ div#action-buttons {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
z-index: 100;
|
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 {
|
span {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin: 0 0.4rem;
|
margin: 0 0.4rem;
|
||||||
|
@ -10,13 +10,20 @@ const appMessages = {
|
|||||||
you_are_going_to_sign: 'Vous allez signer le document',
|
you_are_going_to_sign: 'Vous allez signer le document',
|
||||||
signature_confirmation: 'Confirmation de la signature',
|
signature_confirmation: 'Confirmation de la signature',
|
||||||
sign: 'Signer',
|
sign: 'Signer',
|
||||||
choose_another_signature: 'Choisir une autre zone de signature',
|
choose_another_signature: 'Choisir une autre zone',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
cancel_signing: 'Refuser de signer',
|
cancel_signing: 'Refuser de signer',
|
||||||
last_sign_zone: 'Zone de signature précédente',
|
last_sign_zone: 'Zone de signature précédente',
|
||||||
next_sign_zone: 'Zone de signature suivante',
|
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...',
|
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||||
loading: 'Chargement...'
|
loading: 'Chargement...',
|
||||||
|
remove_sign_zone: 'Enlever la zone',
|
||||||
|
return: 'Retour',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ final readonly class PdfSignedMessage
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly int $signatureId,
|
public readonly int $signatureId,
|
||||||
public readonly int $signatureZoneIndex,
|
public readonly ?int $signatureZoneIndex,
|
||||||
public readonly string $content,
|
public readonly string $content,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ final readonly class RequestPdfSignMessage
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public int $signatureId,
|
public int $signatureId,
|
||||||
public PDFSignatureZone $PDFSignatureZone,
|
public PDFSignatureZone $PDFSignatureZone,
|
||||||
public int $signatureZoneIndex,
|
public ?int $signatureZoneIndex,
|
||||||
public string $reason,
|
public string $reason,
|
||||||
public string $signerText,
|
public string $signerText,
|
||||||
public string $content,
|
public string $content,
|
||||||
|
@ -17,7 +17,7 @@ final readonly class PDFSignatureZone
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
public int $index,
|
public ?int $index,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
public float $x,
|
public float $x,
|
||||||
#[Groups(['read'])]
|
#[Groups(['read'])]
|
||||||
|
@ -12,12 +12,15 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
@ -28,6 +31,7 @@ final readonly class WorkflowAddSignatureController
|
|||||||
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
|
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
|
||||||
private NormalizerInterface $normalizer,
|
private NormalizerInterface $normalizer,
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
|
#[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();
|
$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);
|
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||||
if (null === $storedObject) {
|
if (null === $storedObject) {
|
||||||
throw new NotFoundHttpException('No stored object found');
|
throw new NotFoundHttpException('No stored object found');
|
||||||
|
@ -396,7 +396,10 @@ class WorkflowController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($signature->getSigner() instanceof User) {
|
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);
|
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
||||||
@ -420,7 +423,10 @@ class WorkflowController extends AbstractController
|
|||||||
$this->entityManager->persist($signature);
|
$this->entityManager->persist($signature);
|
||||||
$this->entityManager->flush();
|
$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(
|
return $this->render(
|
||||||
|
@ -25,11 +25,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<div class="content" id="content">
|
<div class="content" id="content">
|
||||||
<div class="container-xxl">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col-12 m-auto">
|
||||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
<div class="row" id="document-signature"></div>
|
||||||
<div class="row" id="document-signature"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,24 +19,7 @@ use Twig\TwigFilter;
|
|||||||
*/
|
*/
|
||||||
class ChillEntityRenderExtension extends AbstractExtension
|
class ChillEntityRenderExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(private readonly ChillEntityRenderManagerInterface $renderManager) {}
|
||||||
* @var ChillEntityRender
|
|
||||||
*/
|
|
||||||
protected $defaultRender;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var iterable|ChillEntityRenderInterface[]
|
|
||||||
*/
|
|
||||||
protected $renders = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ChillEntityRenderExtension constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(iterable $renders)
|
|
||||||
{
|
|
||||||
$this->defaultRender = new ChillEntityRender();
|
|
||||||
$this->renders = $renders;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array|TwigFilter[]
|
* @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 $this->renderManager->renderBox($entity, $options);
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 $this->renderManager->renderString($entity, $options);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ namespace Chill\MainBundle\Templating\Entity;
|
|||||||
* Interface to implement which will render an entity in template on a custom
|
* Interface to implement which will render an entity in template on a custom
|
||||||
* manner.
|
* manner.
|
||||||
*
|
*
|
||||||
* @template T
|
* @template T of object
|
||||||
*/
|
*/
|
||||||
interface ChillEntityRenderInterface
|
interface ChillEntityRenderInterface
|
||||||
{
|
{
|
||||||
@ -31,7 +31,7 @@ interface ChillEntityRenderInterface
|
|||||||
* </span>
|
* </span>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param T $entity
|
* @param T|null $entity
|
||||||
*
|
*
|
||||||
* @phpstan-pure
|
* @phpstan-pure
|
||||||
*/
|
*/
|
||||||
@ -42,7 +42,7 @@ interface ChillEntityRenderInterface
|
|||||||
*
|
*
|
||||||
* Example: returning the name of a person.
|
* Example: returning the name of a person.
|
||||||
*
|
*
|
||||||
* @param T $entity
|
* @param T|null $entity
|
||||||
*
|
*
|
||||||
* @phpstan-pure
|
* @phpstan-pure
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -23,6 +23,7 @@ use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
@ -62,7 +63,9 @@ class WorkflowAddSignatureControllerTest extends TestCase
|
|||||||
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
|
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
|
||||||
->willReturn('ok');
|
->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());
|
$actual = $controller($signature, new Request());
|
||||||
|
|
||||||
|
@ -32,11 +32,16 @@ services:
|
|||||||
- { name: twig.extension }
|
- { name: twig.extension }
|
||||||
|
|
||||||
Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension:
|
Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension:
|
||||||
arguments:
|
|
||||||
$renders: !tagged_iterator chill.render_entity
|
|
||||||
tags:
|
tags:
|
||||||
- { name: twig.extension }
|
- { 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:
|
Chill\MainBundle\Templating\Entity\CommentRender:
|
||||||
tags:
|
tags:
|
||||||
- { name: 'chill.render_entity' }
|
- { name: 'chill.render_entity' }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user