mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-19 21:24:59 +00:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
559901e528 | |||
05bc69fd33 | |||
49da62d364 | |||
3af7824d01
|
|||
033053c437
|
|||
633bb00154
|
|||
f5c1b5cf8a | |||
ccd71da4e4
|
|||
1eadb3bbdb
|
|||
|
0bb5a79cae
|
||
|
bd3198e42b
|
||
|
96dfddc55f
|
||
|
da37a3db5f
|
||
|
c2882b1079
|
||
b9e515f4e6 | |||
|
df2ea7e1ba
|
||
|
d59cda9cc4
|
||
7a98bb5a06
|
|||
2ce8f540fe
|
|||
6c4d8990cc | |||
351e9c3fcc
|
|||
1b65cac1df
|
4
.changes/v3.4.3.md
Normal file
4
.changes/v3.4.3.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v3.4.3 - 2024-12-05
|
||||
### Fixed
|
||||
* Remove the "not null" constraint on person supplementary phones
|
||||
* Remove doctrine annotation that prevent from adding documents to activities
|
6
.changes/v3.5.0.md
Normal file
6
.changes/v3.5.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v3.5.0 - 2024-12-09
|
||||
### Feature
|
||||
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
|
||||
### Fixed
|
||||
* Wrap the signature's change state into a transaction, to avoid race conditions
|
||||
* Fix display of gender label
|
12
CHANGELOG.md
12
CHANGELOG.md
@@ -6,6 +6,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v3.5.0 - 2024-12-09
|
||||
### Feature
|
||||
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
|
||||
### Fixed
|
||||
* Wrap the signature's change state into a transaction, to avoid race conditions
|
||||
* Fix display of gender label
|
||||
|
||||
## v3.4.3 - 2024-12-05
|
||||
### Fixed
|
||||
* Remove the "not null" constraint on person supplementary phones
|
||||
* Remove doctrine annotation that prevent from adding documents to activities
|
||||
|
||||
## v3.4.2 - 2024-12-05
|
||||
### Fixed
|
||||
* ([#329](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/329)) Fix the serialization of gender for the generation of documents
|
||||
|
@@ -95,6 +95,7 @@
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
"phpunit/phpunit": "^10.5.24",
|
||||
"rector/rector": "^1.1.0",
|
||||
"symfony/amqp-messenger": "^5.4.45",
|
||||
"symfony/debug-bundle": "^5.4",
|
||||
"symfony/dotenv": "^5.4",
|
||||
"symfony/flex": "^2.4",
|
||||
|
11
config/packages/chill_workflow_signature_documents.yaml
Normal file
11
config/packages/chill_workflow_signature_documents.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
chill_main:
|
||||
workflow_signature:
|
||||
base_signer:
|
||||
document_kinds:
|
||||
- { key: id_card, labels: [ { lang: fr, label: "Carte d'identité" } ] }
|
||||
- { key: passport, labels: [ { lang: fr, label: "Passeport" } ] }
|
||||
- { key: drivers_license, labels: [ { lang: fr, label: "Permis de conduire" } ] }
|
||||
- { key: visa_long_stay, labels: [ { lang: fr, label: "Visa de long séjour" } ] }
|
||||
- { key: resident_permit, labels: [ { lang: fr, label: "Carte de séjour" } ] }
|
||||
- { key: residency_card, labels: [ { lang: fr, label: "Carte de résident" } ] }
|
||||
- { key: provisionary_residency_permit, labels: [ { lang: fr, label: "Autorisation provisoire de séjour" } ] }
|
@@ -84,11 +84,6 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
*/
|
||||
#[Assert\Valid(traverse: true)]
|
||||
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
|
||||
#[ORM\JoinTable(
|
||||
name: 'activity_storedobject',
|
||||
joinColumns: new ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', nullable: false),
|
||||
inverseJoinColumns: new ORM\InverseJoinColumn(name: 'storedobject_id', referencedColumnName: 'id', unique: true, nullable: false)
|
||||
)]
|
||||
private Collection $documents;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TIME_MUTABLE, nullable: true)]
|
||||
|
@@ -18,8 +18,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||
|
||||
use Monolog\DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* * Lundi => Envoi des rdv du mardi et mercredi.
|
||||
* * Mardi => Envoi des rdv du jeudi.
|
||||
@@ -31,7 +29,7 @@ class DefaultRangeGenerator implements RangeGeneratorInterface
|
||||
{
|
||||
public function generateRange(\DateTimeImmutable $date): ?array
|
||||
{
|
||||
$onMidnight = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d').' 00:00:00');
|
||||
$onMidnight = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d').' 00:00:00');
|
||||
|
||||
switch ($dow = (int) $onMidnight->format('w')) {
|
||||
case 6: // Saturday
|
||||
|
@@ -26,9 +26,9 @@
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<div class="col-12 m-auto">
|
||||
<div class="col-12 m-auto sticky-top">
|
||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||
<div class="col text-center turn-page">
|
||||
<div class="col-5 text-center turn-page">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
@@ -40,26 +40,52 @@
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxMulti"
|
||||
v-model="multiPage"
|
||||
@change="toggleMultiPage"
|
||||
/>
|
||||
<label class="form-check-label" for="checkboxMulti">
|
||||
{{ $t("all_pages") }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1"
|
||||
v-if="signature.zones.length > 0"
|
||||
class="col-5 p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
@@ -96,16 +122,23 @@
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
<button v-if="userSignatureZone === null"
|
||||
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
|
||||
<button
|
||||
v-if="userSignatureZone === null"
|
||||
:class="{
|
||||
btn: true,
|
||||
'btn-sm': true,
|
||||
'btn-create': canvasEvent !== 'add',
|
||||
'btn-chill-green': canvasEvent === 'add',
|
||||
active: canvasEvent === 'add',
|
||||
}"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
<template v-if="canvasEvent === 'add'">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="canvasEvent === 'add'">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +146,7 @@
|
||||
<div
|
||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
||||
>
|
||||
<div class="col-3 text-center turn-page ps-3">
|
||||
<div class="col-5 text-center turn-page ps-3">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
@@ -125,26 +158,35 @@
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>{{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxMulti"
|
||||
v-model="multiPage"
|
||||
@change="toggleMultiPage"
|
||||
/>
|
||||
<label class="form-check-label" for="checkboxMulti">
|
||||
{{ $t("see_all_pages") }}
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
v-if="signature.zones.length > 0 && signedState !== 'signed'"
|
||||
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||
>
|
||||
<button
|
||||
@@ -164,7 +206,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
v-if="signature.zones.length > 0 && signedState !== 'signed'"
|
||||
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
@@ -200,28 +242,45 @@
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
<button v-if="userSignatureZone === null"
|
||||
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
|
||||
<button
|
||||
v-if="userSignatureZone === null"
|
||||
:class="{
|
||||
btn: true,
|
||||
'btn-sm': true,
|
||||
'btn-create': canvasEvent !== 'add',
|
||||
'btn-chill-green': canvasEvent === 'add',
|
||||
active: canvasEvent === 'add',
|
||||
}"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
<template v-if="canvasEvent !== 'add'">
|
||||
{{ $t("add_zone") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("click_on_document")}}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="canvasEvent !== 'add'">
|
||||
{{ $t("add_zone") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("click_on_document") }}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center" :class="{onAddZone: canvasEvent === 'add'}">
|
||||
<canvas class="m-auto" id="canvas" ></canvas>
|
||||
<div
|
||||
v-if="multiPage"
|
||||
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center d-flex flex-column"
|
||||
:class="{ onAddZone: canvasEvent === 'add' }"
|
||||
>
|
||||
<canvas v-for="p in pageCount" :key="p" :id="`canvas-${p}`"></canvas>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center"
|
||||
:class="{ onAddZone: canvasEvent === 'add' }"
|
||||
>
|
||||
<canvas class="m-auto" id="canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
||||
<div class="row">
|
||||
<div class="col d-flex">
|
||||
@@ -281,6 +340,7 @@ import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButt
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||
|
||||
const multiPage: Ref<boolean> = ref(true);
|
||||
const modalOpen: Ref<boolean> = ref(false);
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const adding: Ref<boolean> = ref(false);
|
||||
@@ -347,23 +407,37 @@ const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
|
||||
const setZoomLevel = (zoomLevel: string) => {
|
||||
const setZoomLevel = async (zoomLevel: string) => {
|
||||
zoom.value = Number.parseFloat(zoomLevel);
|
||||
setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
};
|
||||
|
||||
const mountPdf = async (doc: ArrayBuffer) => {
|
||||
const loadingTask = pdfjsLib.getDocument(doc);
|
||||
pdf = await loadingTask.promise;
|
||||
pageCount.value = pdf.numPages;
|
||||
await setPage(page.value);
|
||||
if (multiPage.value) {
|
||||
await setAllPages();
|
||||
} else {
|
||||
await setPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getCanvas = (page: number) =>
|
||||
multiPage.value
|
||||
? (document.getElementById(`canvas-${page}`) as HTMLCanvasElement)
|
||||
: (document.querySelectorAll("canvas")[0] as HTMLCanvasElement);
|
||||
|
||||
const getCanvasId = (canvas: HTMLCanvasElement) => {
|
||||
const number = canvas.id.split("-").pop();
|
||||
return number ? parseInt(number) : 0;
|
||||
};
|
||||
|
||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
const scale = 1 * zoom.value;
|
||||
const viewport = pdfPage.getViewport({ scale });
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
const canvas = getCanvas(pdfPage.pageNumber);
|
||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
@@ -374,6 +448,9 @@ const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
};
|
||||
};
|
||||
|
||||
const setAllPages = async () =>
|
||||
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
|
||||
|
||||
const setPage = async (page: number) => {
|
||||
const pdfPage = await pdf.getPage(page);
|
||||
const renderContext = getRenderContext(pdfPage);
|
||||
@@ -395,10 +472,34 @@ async function downloadAndOpen(): Promise<Blob> {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const addCanvasEvents = () => {
|
||||
if (multiPage.value) {
|
||||
Array.from(Array(pageCount.value).keys()).map((p) => {
|
||||
const canvas = getCanvas(p + 1);
|
||||
canvas.addEventListener(
|
||||
"pointerup",
|
||||
(e) => canvasClick(e, canvas),
|
||||
false
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener("pointerup", (e) => canvasClick(e, canvas), false);
|
||||
}
|
||||
};
|
||||
|
||||
const initPdf = () => {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener("pointerup", canvasClick, false);
|
||||
setTimeout(() => drawAllZones(page.value), 800);
|
||||
addCanvasEvents();
|
||||
setTimeout(drawAllZones, 800);
|
||||
};
|
||||
|
||||
const resetPages = () =>
|
||||
multiPage.value ? setAllPages() : setPage(page.value);
|
||||
|
||||
const toggleMultiPage = async () => {
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
addCanvasEvents();
|
||||
};
|
||||
|
||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
||||
@@ -410,35 +511,36 @@ const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
||||
const hitSignature = (
|
||||
zone: SignatureZone,
|
||||
xy: number[],
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
canvas: HTMLCanvasElement
|
||||
) =>
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||||
scaleXToCanvas(zone.x, canvas.width, zone.PDFPage.width) < xy[0] &&
|
||||
xy[0] <
|
||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||||
scaleXToCanvas(zone.x + zone.width, canvas.width, zone.PDFPage.width) &&
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
||||
scaleYToCanvas(zone.y, canvas.height, zone.PDFPage.height) <
|
||||
xy[1] &&
|
||||
xy[1] <
|
||||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
||||
scaleYToCanvas(zone.height - zone.y, canvas.height, zone.PDFPage.height) +
|
||||
zone.PDFPage.height * zoom.value;
|
||||
|
||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
const selectZone = async (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
userSignatureZone.value = z;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||||
.filter(
|
||||
(z) =>
|
||||
(z.PDFPage.index + 1 === getCanvasId(canvas) && multiPage.value) ||
|
||||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
|
||||
)
|
||||
.map((z) => {
|
||||
if (
|
||||
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
|
||||
) {
|
||||
if (hitSignature(z, [e.offsetX, e.offsetY], canvas)) {
|
||||
if (userSignatureZone.value === null) {
|
||||
selectZone(z, canvas);
|
||||
} else {
|
||||
@@ -449,18 +551,20 @@ const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
}
|
||||
});
|
||||
|
||||
const canvasClick = (e: PointerEvent) => {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
canvasEvent.value === "select"
|
||||
? selectZoneEvent(e, canvas)
|
||||
: addZoneEvent(e, canvas);
|
||||
};
|
||||
|
||||
const turnPage = async (upOrDown: number) => {
|
||||
//userSignatureZone.value = null; // desactivate the reset of the zone when turning page
|
||||
page.value = page.value + upOrDown;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
if (multiPage.value) {
|
||||
const canvas = getCanvas(page.value);
|
||||
canvas.scrollIntoView();
|
||||
} else {
|
||||
await setPage(page.value);
|
||||
setTimeout(drawAllZones, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const turnSignature = async (upOrDown: number) => {
|
||||
@@ -476,9 +580,9 @@ const turnSignature = async (upOrDown: number) => {
|
||||
let currentZone = signature.zones[zoneIndex];
|
||||
if (currentZone) {
|
||||
page.value = currentZone.PDFPage.index + 1;
|
||||
userSignatureZone.value = currentZone;
|
||||
const canvas = document.querySelectorAll("canvas")[0];
|
||||
const canvas = getCanvas(currentZone.PDFPage.index + 1);
|
||||
selectZone(currentZone, canvas);
|
||||
canvas.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -523,19 +627,25 @@ const drawZone = (
|
||||
}
|
||||
};
|
||||
|
||||
const drawAllZones = (page: number) => {
|
||||
const canvas = document.querySelectorAll("canvas")[0];
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx && signedState.value !== "signed") {
|
||||
const drawAllZones = () => {
|
||||
if (signedState.value !== "signed") {
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page)
|
||||
.filter(
|
||||
(z) =>
|
||||
multiPage.value ||
|
||||
(z.PDFPage.index + 1 === page.value && !multiPage.value)
|
||||
)
|
||||
.map((z) => {
|
||||
if (userSignatureZone.value) {
|
||||
if (userSignatureZone.value?.index === z.index) {
|
||||
const canvas = getCanvas(z.PDFPage.index + 1);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
if (userSignatureZone.value) {
|
||||
if (userSignatureZone.value?.index === z.index) {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -621,8 +731,8 @@ const confirmSign = () => {
|
||||
|
||||
const undoSign = async () => {
|
||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||
await setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
userSignatureZone.value = null;
|
||||
adding.value = false;
|
||||
canvasEvent.value = "select";
|
||||
@@ -648,13 +758,13 @@ const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
||||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
||||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
||||
y:
|
||||
PDFPageHeight -
|
||||
PDFPageHeight * zoom.value -
|
||||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
||||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
||||
width: BOX_WIDTH,
|
||||
height: BOX_HEIGHT,
|
||||
width: BOX_WIDTH * zoom.value,
|
||||
height: BOX_HEIGHT * zoom.value,
|
||||
PDFPage: {
|
||||
index: page.value - 1,
|
||||
index: multiPage.value ? getCanvasId(canvas) - 1 : page.value - 1,
|
||||
width: PDFPageWidth,
|
||||
height: PDFPageHeight,
|
||||
},
|
||||
@@ -662,8 +772,8 @@ const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
||||
signature.zones.push(newZone);
|
||||
userSignatureZone.value = newZone;
|
||||
|
||||
await setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
await resetPages();
|
||||
setTimeout(drawAllZones, 200);
|
||||
canvasEvent.value = "select";
|
||||
adding.value = true;
|
||||
};
|
||||
@@ -678,16 +788,17 @@ init();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#canvas {
|
||||
canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.onAddZone {
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed;
|
||||
|
||||
#canvas {
|
||||
cursor: copy;
|
||||
}
|
||||
canvas {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
div#action-buttons {
|
||||
@@ -699,6 +810,10 @@ div#action-buttons {
|
||||
div.pdf-tools {
|
||||
background-color: #f3f3f3;
|
||||
font-size: 0.6rem;
|
||||
label {
|
||||
font-size: 0.75rem !important;
|
||||
margin: auto 0 auto 0.3rem;
|
||||
}
|
||||
button {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
@@ -24,6 +24,8 @@ const appMessages = {
|
||||
loading: 'Chargement...',
|
||||
remove_sign_zone: 'Enlever la zone',
|
||||
return: 'Retour',
|
||||
see_all_pages: 'Voir toutes les pages',
|
||||
all_pages: 'Toutes les pages',
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,7 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowSignatureCancelController
|
||||
final readonly class WorkflowSignatureStateChangeController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
@@ -79,8 +79,9 @@ final readonly class WorkflowSignatureCancelController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$markSignature($signature);
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
|
||||
$markSignature($signature);
|
||||
});
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
@@ -14,10 +14,13 @@ namespace Chill\MainBundle\Tests\Workflow;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
@@ -47,7 +50,12 @@ class SignatureStepStateChangerTest extends TestCase
|
||||
$user = new User();
|
||||
|
||||
$messengerBus = new MessageBus([]);
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), $messengerBus);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects($this->exactly(4))->method('refresh')->with(
|
||||
$this->isInstanceOf(EntityWorkflowStepSignature::class),
|
||||
$this->logicalOr(LockMode::PESSIMISTIC_WRITE, LockMode::PESSIMISTIC_READ)
|
||||
);
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), $messengerBus, $entityManager);
|
||||
|
||||
// move it to signature
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
@@ -94,7 +102,12 @@ class SignatureStepStateChangerTest extends TestCase
|
||||
$user = new User();
|
||||
|
||||
$messengerBus = new MessageBus([]);
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), $messengerBus);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects($this->exactly(2))->method('refresh')->with(
|
||||
$this->isInstanceOf(EntityWorkflowStepSignature::class),
|
||||
$this->logicalOr(LockMode::PESSIMISTIC_WRITE, LockMode::PESSIMISTIC_READ)
|
||||
);
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), $messengerBus, $entityManager);
|
||||
|
||||
// move it to signature
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
@@ -126,7 +139,12 @@ class SignatureStepStateChangerTest extends TestCase
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
$clock = new MockClock();
|
||||
$user = new User();
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), new MessageBus([]));
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$entityManager->expects($this->exactly(2))->method('refresh')->with(
|
||||
$this->isInstanceOf(EntityWorkflowStepSignature::class),
|
||||
$this->logicalOr(LockMode::PESSIMISTIC_WRITE, LockMode::PESSIMISTIC_READ)
|
||||
);
|
||||
$changer = new SignatureStepStateChanger($registry, $clock, new NullLogger(), new MessageBus([]), $entityManager);
|
||||
|
||||
// move it to signature
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
|
@@ -33,9 +33,12 @@ final readonly class PostSignatureStateChangeHandler implements MessageHandlerIn
|
||||
throw new UnrecoverableMessageHandlingException('signature not found');
|
||||
}
|
||||
|
||||
$this->signatureStepStateChanger->onPostMark($signature);
|
||||
$this->entityManager->wrapInTransaction(function () use ($signature) {
|
||||
$this->signatureStepStateChanger->onPostMark($signature);
|
||||
|
||||
$this->entityManager->flush();
|
||||
});
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
}
|
||||
|
@@ -16,11 +16,16 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
/**
|
||||
* Handles state changes for signature steps within a workflow.
|
||||
*/
|
||||
class SignatureStepStateChanger
|
||||
{
|
||||
private const LOG_PREFIX = '[SignatureStepStateChanger] ';
|
||||
@@ -30,10 +35,26 @@ class SignatureStepStateChanger
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Marks a signature as signed.
|
||||
*
|
||||
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
||||
* transaction.
|
||||
*
|
||||
* This method updates the state of the provided signature entity, sets the signature index,
|
||||
* and logs the action. Additionally, it dispatches a message indicating that the signature
|
||||
* state has changed.
|
||||
*
|
||||
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
|
||||
* @param int|null $atIndex optional index position for the signature within the zone
|
||||
*/
|
||||
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$signature
|
||||
->setState(EntityWorkflowSignatureStateEnum::SIGNED)
|
||||
->setZoneSignatureIndex($atIndex)
|
||||
@@ -42,8 +63,19 @@ class SignatureStepStateChanger
|
||||
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a signature as canceled.
|
||||
*
|
||||
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
||||
* transaction.
|
||||
*
|
||||
* This method updates the signature state to 'canceled' and logs the action.
|
||||
* It also dispatches a message to notify about the state change.
|
||||
*/
|
||||
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$signature
|
||||
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
|
||||
->setStateDate($this->clock->now());
|
||||
@@ -51,8 +83,21 @@ class SignatureStepStateChanger
|
||||
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given signature as rejected and updates its state and state date accordingly.
|
||||
*
|
||||
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
||||
* transaction.
|
||||
*
|
||||
* This method logs the rejection of the signature and dispatches a message indicating
|
||||
* a state change has occurred.
|
||||
*
|
||||
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
|
||||
*/
|
||||
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
$signature
|
||||
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
|
||||
->setStateDate($this->clock->now());
|
||||
@@ -63,10 +108,15 @@ class SignatureStepStateChanger
|
||||
/**
|
||||
* Executed after a signature has a new state.
|
||||
*
|
||||
* This method will acquire a lock on the database side, so it must be wrapped into an explicit
|
||||
* transaction.
|
||||
*
|
||||
* This should be executed only by a system user (without any user registered)
|
||||
*/
|
||||
public function onPostMark(EntityWorkflowStepSignature $signature): void
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
|
||||
|
||||
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
|
||||
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
|
||||
|
||||
|
@@ -0,0 +1,32 @@
|
||||
<?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\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20241205140905 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Restore the nullable person phone, as this is used by comments';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this should be already not null, but helps some instances, where a migration which drop not null
|
||||
// was executed, and was wrong.
|
||||
$this->addSql('ALTER TABLE chill_person_phone ALTER phonenumber DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void {}
|
||||
}
|
@@ -20,7 +20,6 @@ use libphonenumber\PhoneNumber;
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_person_phone')]
|
||||
#[ORM\Index(name: 'phonenumber', columns: ['phonenumber'])]
|
||||
#[ORM\Index(name: 'phonenumber', columns: ['phonenumber'])]
|
||||
class PersonPhone
|
||||
{
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, nullable: false)]
|
||||
@@ -37,7 +36,12 @@ class PersonPhone
|
||||
#[ORM\ManyToOne(targetEntity: Person::class, inversedBy: 'otherPhoneNumbers')]
|
||||
private Person $person;
|
||||
|
||||
#[ORM\Column(type: 'phone_number', nullable: false)]
|
||||
/**
|
||||
* The phonenumber.
|
||||
*
|
||||
* This phonenumber is nullable: this allow user to store some notes instead of a phonenumber
|
||||
*/
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, length: 40, nullable: true)]
|
||||
|
@@ -80,7 +80,7 @@
|
||||
>
|
||||
<option selected disabled >{{ $t('person.gender.placeholder') }}</option>
|
||||
<option v-for="g in config.genders" :value="g.id" :key="g.id">
|
||||
{{ g.label.fr }}
|
||||
{{ g.label }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{ $t('person.gender.title') }}</label>
|
||||
@@ -337,6 +337,7 @@ export default {
|
||||
getGenders()
|
||||
.then(genders => {
|
||||
if ('results' in genders) {
|
||||
console.log('genders', genders.results)
|
||||
this.config.genders = genders.results;
|
||||
}
|
||||
});
|
||||
|
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Controller;
|
||||
|
||||
use Chill\MainBundle\Controller\WorkflowSignatureCancelController;
|
||||
use Chill\MainBundle\Controller\WorkflowSignatureStateChangeController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
@@ -72,7 +72,7 @@ class WorkflowSignatureCancelControllerStepTest extends WebTestCase
|
||||
$twig->expects($this->once())->method('render')->withAnyParameters()
|
||||
->willReturn('template');
|
||||
|
||||
$controller = new WorkflowSignatureCancelController($entityManager, $security, $this->formFactory, $twig, $this->signatureStepStateChanger, $this->chillUrlGenerator);
|
||||
$controller = new WorkflowSignatureStateChangeController($entityManager, $security, $this->formFactory, $twig, $this->signatureStepStateChanger, $this->chillUrlGenerator);
|
||||
|
||||
$request = new Request();
|
||||
$request->setMethod('GET');
|
||||
|
@@ -29,7 +29,6 @@ final class Version20240918151852 extends AbstractMigration
|
||||
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_referrer ALTER id DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_person_household_composition_type ALTER label SET DEFAULT \'{}\'');
|
||||
$this->addSql('ALTER TABLE chill_person_household_composition_type ALTER label SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_phone ALTER phonenumber SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_relationships ALTER createdby_id DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_relationships ALTER createdat DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_resource ALTER createdby_id DROP NOT NULL');
|
||||
|
Reference in New Issue
Block a user