Compare commits

..

22 Commits

Author SHA1 Message Date
559901e528 Update chill bundles version to v3.5.0 2024-12-09 11:35:16 +01:00
05bc69fd33 Fix display of gender label
Label already localized in serialization so not to be accessed as label.fr within vue component
2024-12-09 11:32:28 +01:00
49da62d364 Merge branch 'signature-state-change-wrapped-into-transaction' into 'master'
Refactor transaction handling for signature state changes, to wrap them into transactions

See merge request Chill-Projet/chill-bundles!771
2024-12-06 11:43:08 +00:00
3af7824d01 Refactor transaction handling for signature state changes, to wrap them into transactions
Wrap signature state changes in transactions to prevent race conditions and ensure data integrity. Update controller and test class names to reflect broader state change capabilities. Enhance documentation with comments to clarify transaction requirements and procedure details for signature operations.
2024-12-06 12:37:17 +01:00
033053c437 Restrict the version of amqp-messenger 2024-12-06 12:37:16 +01:00
633bb00154 fix typing of Datetime 2024-12-06 12:37:16 +01:00
f5c1b5cf8a Merge branch '318-signature-vue-app-show-full-doc' into 'master'
Resolve "[App de signature] Pouvoir voir le document en continu (toutes les pages ensemble)"

Closes #318

See merge request Chill-Projet/chill-bundles!761
2024-12-05 16:31:04 +00:00
ccd71da4e4 Add changie 2024-12-05 17:25:08 +01:00
1eadb3bbdb add missing configuration for chill_workflow_signature_documents.yaml 2024-12-05 17:16:06 +01:00
nobohan
0bb5a79cae FEATURE signature: show full pages - show next signature button even if one signature 2024-12-05 17:16:06 +01:00
nobohan
bd3198e42b FEATURE signature: show full pages - UI and turn pages 2024-12-05 17:16:05 +01:00
nobohan
96dfddc55f FEATURE signature: show full pages - can add zone + fix hitSignature is canvas-aware 2024-12-05 17:16:05 +01:00
nobohan
da37a3db5f FEATURE signature: show full pages - can select zone 2024-12-05 17:16:04 +01:00
nobohan
c2882b1079 FEATURE signature: show full pages - WIP 2024-12-05 17:16:01 +01:00
b9e515f4e6 Merge branch '317-add-zone-bug-with-zoom' into 'master'
Resolve "Lorsqu'on place manuellement une zone de signature avec le zoom actif, la zone n'est pas placée correctement sur la page"

Closes #317

See merge request Chill-Projet/chill-bundles!757
2024-12-05 15:49:38 +00:00
nobohan
df2ea7e1ba FIXED adding zone in signature vue app was not placed correctly when zooming 2024-12-05 16:41:51 +01:00
nobohan
d59cda9cc4 DX format signature vue app 2024-12-05 16:41:51 +01:00
7a98bb5a06 release v3.4.3 2024-12-05 15:44:51 +01:00
2ce8f540fe release v3.4.3 2024-12-05 15:34:28 +01:00
6c4d8990cc Merge branch '342-error-while-deploying-chill-version-v3-4-2-migrations-are-blocked' into 'master'
Resolve "Error while deploying chill version v3.4.2: migrations are blocked"

Closes #342

See merge request Chill-Projet/chill-bundles!770
2024-12-05 14:33:34 +00:00
351e9c3fcc Remove custom join table configuration for documents
The custom join table setup for the 'documents' relation has been removed. This change relies on the default naming and configuration provided by Doctrine, simplifying the code and reducing potential configuration errors.
2024-12-05 15:32:35 +01:00
1b65cac1df Make person phone number nullable
Removed the "not null" constraint from the person phone number field to allow for better flexibility in data storage, such as storing notes. This change rectifies issues in certain instances where the migration had incorrectly set the field to "not null". Adjustments include updating the database schema and modifying the entity definition to reflect this change.
2024-12-05 15:30:25 +01:00
18 changed files with 386 additions and 134 deletions

4
.changes/v3.4.3.md Normal file
View 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
View 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

View File

@@ -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

View File

@@ -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",

View 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" } ] }

View File

@@ -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)]

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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',
}
}

View File

@@ -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()])

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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()]);

View File

@@ -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 {}
}

View File

@@ -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)]

View File

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

View File

@@ -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');

View File

@@ -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');