Merge branch 'master' into 339-partage-d'export-enregistré

# Conflicts:
#	src/Bundle/ChillMainBundle/Resources/views/Dev/dev.assets.html.twig
This commit is contained in:
Julien Fastré 2025-04-24 14:26:06 +02:00
commit aa44577484
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
48 changed files with 770 additions and 237 deletions

19
.changes/v3.11.0.md Normal file
View File

@ -0,0 +1,19 @@
## v3.11.0 - 2025-04-17
### Feature
* ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities.
* ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number
**Schema Change**: Add columns or tables
* Signature: add a button to go directly to the signature zone, even if there is only one
### Fixed
* Fixed wrong translations in the on-the-fly for creation of thirdParty
* Fixed update of phone number in on-the-fly edition of thirdParty
* Fixed closing of modal when editing thirdParty in accompanying course works
* Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day
* ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list
* When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk
* Add consistent log prefix and key to logs when stale workflows are automatically canceled
* ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition
### DX
* Add new chill-col style for displaying title and aside in a flex table

View File

@ -220,6 +220,7 @@ framework:
- attenteModification - attenteModification
- attenteMiseEnForme - attenteMiseEnForme
- attenteValidationMiseEnForme - attenteValidationMiseEnForme
- attenteSignature
- attenteVisa - attenteVisa
- postSignature - postSignature
- attenteTraitement - attenteTraitement

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Menu; namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@ -23,22 +24,30 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
public function __construct(protected Security $security, protected TranslatorInterface $translator) {} public function __construct(
protected Security $security,
protected TranslatorInterface $translator,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {
$period = $parameters['accompanyingCourse']; $period = $parameters['accompanyingCourse'];
$activities = $this->managerRegistry->getManager()->getRepository(Activity::class)->findBy(
['accompanyingPeriod' => $period]
);
if ( if (
AccompanyingPeriod::STEP_DRAFT !== $period->getStep() AccompanyingPeriod::STEP_DRAFT !== $period->getStep()
&& $this->security->isGranted(ActivityVoter::SEE, $period) && $this->security->isGranted(ActivityVoter::SEE, $period)
) { ) {
$menu->addChild($this->translator->trans('Activity'), [ $menu->addChild($this->translator->trans('Activities'), [
'route' => 'chill_activity_activity_list', 'route' => 'chill_activity_activity_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 40]); ->setExtras(['order' => 40, 'counter' => count($activities) > 0 ? count($activities) : null]);
} }
} }

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Menu; namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@ -23,13 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface
{ {
public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private TranslatorInterface $translator) {} public function __construct(
private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
private AuthorizationCheckerInterface $authorizationChecker,
private TranslatorInterface $translator,
) {}
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {
/** @var Person $person */ /** @var Person $person */
$person = $parameters['person']; $person = $parameters['person'];
$count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE);
if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) { if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
$menu->addChild( $menu->addChild(
$this->translator->trans('Activities'), $this->translator->trans('Activities'),
@ -38,7 +46,7 @@ final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface
'routeParameters' => ['person_id' => $person->getId()], 'routeParameters' => ['person_id' => $person->getId()],
] ]
) )
->setExtra('order', 201); ->setExtras(['order' => 201, 'counter' => $count > 0 ? $count : null]);
} }
} }

View File

@ -120,3 +120,34 @@ li.document-list-item {
vertical-align: baseline; vertical-align: baseline;
} }
} }
.badge-activity-type-simple {
@extend .badge;
display: inline-block;
margin: 0.2rem 0;
padding-left: 0;
padding-right: 0.5rem;
border-left: 20px groove #9acd32;
border-radius: $badge-border-radius;
color: black;
font-weight: normal;
font-size: unset;
max-width: 100%;
background-color: $gray-100;
overflow: hidden;
text-overflow: ellipsis;
text-indent: 5px hanging;
text-align: left;
&::before {
margin-right: 3px;
position: relative;
left: -0.5px;
font-family: ForkAwesome;
content: '\f04b';
color: #9acd32;
}
}

View File

@ -13,44 +13,44 @@
{% endif %} {% endif %}
<div class="item-row"> <div class="item-row">
<div class="item-col" style="width: unset"> <div class="item-two-col-grid">
{% if document.isPending %} <div class="title">
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> {% if document.isPending %}
{% elseif document.isFailure %} <div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> {% elseif document.isFailure %}
{% endif %} <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
<div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %} {% endif %}
<div class="badge-activity-type">
<span class="title_label"></span> <div>
<span class="title_action"> <div>
{{ activity.type.name | localize_translatable_string }} <div class="badge-activity-type-simple">
{{ activity.type.name | localize_translatable_string }}
</div>
{% if activity.emergency %} {% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span> <span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %} {% endif %}
</span> </div>
</div> </div>
</div> <div class="denomination h2">
<div class="denomination h2"> {{ document.title|chill_print_or_message("No title") }}
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div> </div>
{% endif %} {% if document.hasTemplate %}
</div> <div>
<p>{{ document.template.name|localize_translatable_string }}</p>
<div class="item-col"> </div>
<div class="container"> {% endif %}
</div>
<div class="aside">
<div class="dates row text-end"> <div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span> <span>{{ document.createdAt|format_date('short') }}</span>
</div> </div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<div class="text-end">
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
@import '~ChillPersonAssets/chill/scss/mixins.scss';
@import '~ChillMainAssets/module/bootstrap/shared'; @import '~ChillMainAssets/module/bootstrap/shared';
@import '~ChillPersonAssets/chill/scss/mixins.scss';
@import 'bootstrap/scss/_badge.scss';
.badge-calendar { .badge-calendar {
display: inline-block; display: inline-block;
@ -23,3 +24,35 @@
} }
} }
.badge-calendar-simple {
@extend .badge;
display: inline-block;
margin: 0.2rem 0;
padding-left: 0;
padding-right: 0.5rem;
border-left: 20px groove $chill-l-gray;
border-radius: $badge-border-radius;
max-width: 100%;
background-color: $gray-100;
color: black;
font-weight: normal;
overflow: hidden;
font-weight: normal;
font-size: unset;
text-overflow: ellipsis;
text-indent: 5px hanging;
text-align: left;
&::before {
margin-right: 3px;
position: relative;
left: -0.5px;
font-family: ForkAwesome;
content: '\f04b';
color: $chill-l-gray;
}
}

View File

@ -16,7 +16,7 @@ div.calendar-list {
} }
& > a.calendar-list__global { & > a.calendar-list__global {
display: inline-block;; display: inline-block;
padding: 0.2rem; padding: 0.2rem;
min-width: 2rem; min-width: 2rem;
border: 1px solid var(--bs-chill-blue); border: 1px solid var(--bs-chill-blue);

View File

@ -6,50 +6,48 @@
<div class="item-row"> <div class="item-row">
<div class="item-col" style="width: unset"> <div class="item-two-col-grid">
{% if document.storedObject.isPending %} <div class="title">
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> {% if document.storedObject.isPending %}
{% elseif document.storedObject.isFailure %} <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> {% elseif document.storedObject.isFailure %}
{% endif %} <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
<div>
{% if c.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %} {% endif %}
<span class="badge-calendar">
<span class="title_label"></span>
<span class="title_action">
{{ 'Calendar'|trans }}
{% if c.endDate.diff(c.startDate).days >= 1 %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('short', 'short') }}
{% else %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('none', 'short') }}
{% endif %}
</span>
</span>
</div>
<div class="denomination h2">
{{ document.storedObject.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.hasTemplate %}
<div> <div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col"> <span class="badge-calendar-simple">
<div class="container"> {{ 'Calendar'|trans }}
{% if c.endDate.diff(c.startDate).days >= 1 %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('short', 'short') }}
{% else %}
{{ c.startDate|format_datetime('short', 'short') }}
- {{ c.endDate|format_datetime('none', 'short') }}
{% endif %}
</span>
</div>
<div class="denomination h2">
{{ document.storedObject.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.hasTemplate %}
<div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="aside">
<div class="dates row text-end"> <div class="dates row text-end">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span> <span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</div> </div>
{% if c.accompanyingPeriod is not null and context == 'person' %}
<div class="text-end">
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ c.accompanyingPeriod.id }}
</span>&nbsp;
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,26 +2,28 @@
<teleport to="body"> <teleport to="body">
<modal v-if="modalOpen" @close="modalOpen = false"> <modal v-if="modalOpen" @close="modalOpen = false">
<template v-slot:header> <template v-slot:header>
<h2>{{ $t("signature_confirmation") }}</h2> <h2>{{ trans(SIGNATURES_SIGNATURE_CONFIRMATION) }}</h2>
</template> </template>
<template v-slot:body> <template v-slot:body>
<div class="signature-modal-body text-center" v-if="loading"> <div class="signature-modal-body text-center" v-if="loading">
<p>{{ $t("electronic_signature_in_progress") }}</p> <p>
{{ trans(SIGNATURES_ELECTRONIC_SIGNATURE_IN_PROGRESS) }}
</p>
<div class="loading"> <div class="loading">
<i <i
class="fa fa-circle-o-notch fa-spin fa-3x" class="fa fa-circle-o-notch fa-spin fa-3x"
:title="$t('loading')" :title="trans(SIGNATURES_LOADING)"
></i> ></i>
</div> </div>
</div> </div>
<div class="signature-modal-body text-center" v-else> <div class="signature-modal-body text-center" v-else>
<p>{{ $t("you_are_going_to_sign") }}</p> <p>{{ trans(SIGNATURES_YOU_ARE_GOING_TO_SIGN) }}</p>
<p>{{ $t("are_you_sure") }}</p> <p>{{ trans(SIGNATURES_ARE_YOU_SURE) }}</p>
</div> </div>
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<button class="btn btn-action" @click.prevent="confirmSign"> <button class="btn btn-action" @click.prevent="confirmSign">
{{ $t("yes") }} {{ trans(SIGNATURES_YES) }}
</button> </button>
</template> </template>
</modal> </modal>
@ -82,28 +84,39 @@
@change="toggleMultiPage" @change="toggleMultiPage"
/> />
<label class="form-check-label" for="checkboxMulti"> <label class="form-check-label" for="checkboxMulti">
{{ $t("all_pages") }} {{ trans(SIGNATURES_ALL_PAGES) }}
</label> </label>
</template> </template>
</div> </div>
<div <div
v-if="signature.zones.length > 0" v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-5 p-0 text-center turnSignature" class="col-5 p-0 text-center turnSignature"
> >
<button <button
:disabled="isFirstSignatureZone" class="btn btn-light btn-sm"
@click="goToSignatureZoneUnique"
>
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button>
</div>
<div
v-if="signature.zones.length > 1"
class="col-5 p-0 text-center turnSignature"
>
<button
:disabled="isFirstSignatureZone()"
class="btn btn-light btn-sm" class="btn btn-light btn-sm"
@click="turnSignature(-1)" @click="turnSignature(-1)"
> >
{{ $t("last_zone") }} {{ trans(SIGNATURES_LAST_ZONE) }}
</button> </button>
<span>|</span> <span>|</span>
<button <button
:disabled="isLastSignatureZone" :disabled="isLastSignatureZone()"
class="btn btn-light btn-sm" class="btn btn-light btn-sm"
@click="turnSignature(1)" @click="turnSignature(1)"
> >
{{ $t("next_zone") }} {{ trans(SIGNATURES_NEXT_ZONE) }}
</button> </button>
</div> </div>
<div class="col text-end" v-if="signedState !== 'signed'"> <div class="col text-end" v-if="signedState !== 'signed'">
@ -112,9 +125,9 @@
:hidden="!userSignatureZone" :hidden="!userSignatureZone"
@click="undoSign" @click="undoSign"
v-if="signature.zones.length > 1" v-if="signature.zones.length > 1"
:title="$t('choose_another_signature')" :title="trans(SIGNATURES_CHOOSE_ANOTHER_SIGNATURE)"
> >
{{ $t("another_zone") }} {{ trans(SIGNATURES_ANOTHER_ZONE) }}
</button> </button>
<button <button
class="btn btn-misc btn-sm" class="btn btn-misc btn-sm"
@ -122,7 +135,7 @@
@click="undoSign" @click="undoSign"
v-else v-else
> >
{{ $t("cancel") }} {{ trans(SIGNATURES_CANCEL) }}
</button> </button>
<button <button
v-if="userSignatureZone === null" v-if="userSignatureZone === null"
@ -134,7 +147,7 @@
active: canvasEvent === 'add', active: canvasEvent === 'add',
}" }"
@click="toggleAddZone()" @click="toggleAddZone()"
:title="$t('add_sign_zone')" :title="trans(SIGNATURES_ADD_SIGN_ZONE)"
> >
<template v-if="canvasEvent === 'add'"> <template v-if="canvasEvent === 'add'">
<div <div
@ -186,48 +199,70 @@
@change="toggleMultiPage" @change="toggleMultiPage"
/> />
<label class="form-check-label" for="checkboxMulti"> <label class="form-check-label" for="checkboxMulti">
{{ $t("see_all_pages") }} {{ trans(SIGNATURES_SEE_ALL_PAGES) }}
</label> </label>
</template> </template>
</div> </div>
<div <div
v-if="signature.zones.length > 0 && signedState !== 'signed'" v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-4 d-xl-none text-center turnSignature p-0" class="col-4 d-xl-none text-center turnSignature p-0"
> >
<button <button
:disabled="!hasSignatureZoneSelected"
class="btn btn-light btn-sm" class="btn btn-light btn-sm"
@click="turnSignature(-1)" @click="goToSignatureZoneUnique"
> >
{{ $t("last_zone") }} {{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button>
<span>|</span>
<button
:disabled="isLastSignatureZone"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ $t("next_zone") }}
</button> </button>
</div> </div>
<div <div
v-if="signature.zones.length > 0 && signedState !== 'signed'" v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature" class="col-4 d-xl-none text-center turnSignature p-0"
> >
<button <button
:disabled="isFirstSignatureZone" :disabled="isFirstSignatureZone()"
class="btn btn-light btn-sm" class="btn btn-light btn-sm"
@click="turnSignature(-1)" @click="turnSignature(-1)"
> >
{{ $t("last_sign_zone") }} {{ trans(SIGNATURES_LAST_ZONE) }}
</button> </button>
<span>|</span> <span>|</span>
<button <button
:disabled="isLastSignatureZone" :disabled="isLastSignatureZone()"
class="btn btn-light btn-sm" class="btn btn-light btn-sm"
@click="turnSignature(1)" @click="turnSignature(1)"
> >
{{ $t("next_sign_zone") }} {{ trans(SIGNATURES_NEXT_ZONE) }}
</button>
</div>
<div
v-if="signature.zones.length === 1 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
>
<button
class="btn btn-light btn-sm"
@click="goToSignatureZoneUnique"
>
{{ trans(SIGNATURES_GO_TO_SIGNATURE_UNIQUE) }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
>
<button
:disabled="isFirstSignatureZone()"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ trans(SIGNATURES_LAST_SIGN_ZONE) }}
</button>
<span>|</span>
<button
:disabled="isLastSignatureZone()"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ trans(SIGNATURES_NEXT_SIGN_ZONE) }}
</button> </button>
</div> </div>
<div class="col text-end" v-if="signedState !== 'signed'"> <div class="col text-end" v-if="signedState !== 'signed'">
@ -237,7 +272,7 @@
@click="undoSign" @click="undoSign"
v-if="signature.zones.length > 1" v-if="signature.zones.length > 1"
> >
{{ $t("choose_another_signature") }} {{ trans(SIGNATURES_CHOOSE_ANOTHER_SIGNATURE) }}
</button> </button>
<button <button
class="btn btn-misc btn-sm" class="btn btn-misc btn-sm"
@ -245,7 +280,7 @@
@click="undoSign" @click="undoSign"
v-else v-else
> >
{{ $t("cancel") }} {{ trans(SIGNATURES_CANCEL) }}
</button> </button>
<button <button
v-if="userSignatureZone === null" v-if="userSignatureZone === null"
@ -257,13 +292,13 @@
active: canvasEvent === 'add', active: canvasEvent === 'add',
}" }"
@click="toggleAddZone()" @click="toggleAddZone()"
:title="$t('add_sign_zone')" :title="trans(SIGNATURES_ADD_SIGN_ZONE)"
> >
<template v-if="canvasEvent !== 'add'"> <template v-if="canvasEvent !== 'add'">
{{ $t("add_zone") }} {{ trans(SIGNATURES_ADD_ZONE) }}
</template> </template>
<template v-else> <template v-else>
{{ $t("click_on_document") }} {{ trans(SIGNATURES_CLICK_ON_DOCUMENT) }}
<div <div
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
@ -297,10 +332,10 @@
v-if="signedState !== 'signed'" v-if="signedState !== 'signed'"
:href="getReturnPath()" :href="getReturnPath()"
> >
{{ $t("cancel") }} {{ trans(SIGNATURES_CANCEL) }}
</a> </a>
<a class="btn btn-misc" v-else :href="getReturnPath()"> <a class="btn btn-misc" v-else :href="getReturnPath()">
{{ $t("return") }} {{ trans(SIGNATURES_RETURN) }}
</a> </a>
</div> </div>
<div class="col text-end" v-if="signedState !== 'signed'"> <div class="col text-end" v-if="signedState !== 'signed'">
@ -309,7 +344,7 @@
:disabled="!userSignatureZone" :disabled="!userSignatureZone"
@click="sign" @click="sign"
> >
{{ $t("sign") }} {{ trans(SIGNATURES_SIGN) }}
</button> </button>
</div> </div>
<div class="col-4" v-else></div> <div class="col-4" v-else></div>
@ -318,7 +353,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, Ref, computed } from "vue"; import { ref, Ref } 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 { import {
@ -329,13 +364,38 @@ import {
SignedState, SignedState,
ZoomLevel, ZoomLevel,
} from "../../types"; } from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import * as pdfjsLib from "pdfjs-dist"; import * as pdfjsLib from "pdfjs-dist";
import { import {
PDFDocumentProxy, PDFDocumentProxy,
PDFPageProxy, PDFPageProxy,
} from "pdfjs-dist/types/src/display/api"; } from "pdfjs-dist/types/src/display/api";
import {
SIGNATURES_YES,
SIGNATURES_ARE_YOU_SURE,
SIGNATURES_YOU_ARE_GOING_TO_SIGN,
SIGNATURES_SIGNATURE_CONFIRMATION,
SIGNATURES_SIGN,
SIGNATURES_CHOOSE_ANOTHER_SIGNATURE,
SIGNATURES_CANCEL,
SIGNATURES_LAST_SIGN_ZONE,
SIGNATURES_NEXT_SIGN_ZONE,
SIGNATURES_ADD_SIGN_ZONE,
SIGNATURES_CLICK_ON_DOCUMENT,
SIGNATURES_LAST_ZONE,
SIGNATURES_NEXT_ZONE,
SIGNATURES_ADD_ZONE,
SIGNATURES_ANOTHER_ZONE,
SIGNATURES_ELECTRONIC_SIGNATURE_IN_PROGRESS,
SIGNATURES_LOADING,
SIGNATURES_RETURN,
SIGNATURES_SEE_ALL_PAGES,
SIGNATURES_ALL_PAGES,
SIGNATURES_GO_TO_SIGNATURE_UNIQUE,
trans,
} from "translator";
// @ts-ignore incredible but the console.log is needed // @ts-ignore incredible but the console.log is needed
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs"; import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
console.log(PdfWorker); console.log(PdfWorker);
@ -416,19 +476,15 @@ const $toast = useToast();
const signature = window.signature; const signature = window.signature;
const isFirstSignatureZone = () => const isFirstSignatureZone = () =>
userSignatureZone.value?.index ? userSignatureZone.value.index < 1 : false; userSignatureZone.value?.index != null
? userSignatureZone.value.index < 1
: false;
const isLastSignatureZone = () => const isLastSignatureZone = () =>
userSignatureZone.value?.index userSignatureZone.value?.index
? userSignatureZone.value.index >= signature.zones.length - 1 ? userSignatureZone.value.index >= signature.zones.length - 1
: false; : false;
/**
* Return true if the user has selected a user zone (existing on the doc or created by the user)
*/
const hasSignatureZoneSelected = computed<boolean>(
() => userSignatureZone.value !== null,
);
const setZoomLevel = async (zoomLevel: string) => { const setZoomLevel = async (zoomLevel: string) => {
zoom.value = Number.parseFloat(zoomLevel); zoom.value = Number.parseFloat(zoomLevel);
await resetPages(); await resetPages();
@ -600,6 +656,15 @@ const turnPage = async (upOrDown: number) => {
} }
}; };
const selectZoneInCanvas = (signatureZone: SignatureZone) => {
page.value = signatureZone.PDFPage.index + 1;
const canvas = getCanvas(signatureZone.PDFPage.index + 1);
selectZone(signatureZone, canvas);
canvas.scrollIntoView();
};
const goToSignatureZoneUnique = () => selectZoneInCanvas(signature.zones[0]);
const turnSignature = async (upOrDown: number) => { const turnSignature = async (upOrDown: number) => {
let zoneIndex = userSignatureZone.value?.index ?? -1; let zoneIndex = userSignatureZone.value?.index ?? -1;
if (zoneIndex < -1) { if (zoneIndex < -1) {
@ -612,10 +677,7 @@ const turnSignature = async (upOrDown: number) => {
} }
let currentZone = signature.zones[zoneIndex]; let currentZone = signature.zones[zoneIndex];
if (currentZone) { if (currentZone) {
page.value = currentZone.PDFPage.index + 1; selectZoneInCanvas(currentZone);
const canvas = getCanvas(currentZone.PDFPage.index + 1);
selectZone(currentZone, canvas);
canvas.scrollIntoView();
} }
}; };

View File

@ -23,7 +23,7 @@ License * along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ encore_entry_link_tags("mod_document_action_buttons_group") }} {{ encore_entry_link_tags("mod_document_action_buttons_group") }}
{% endblock %} {% block content %} {% endblock %} {% block content %}
<div class="col-md-10 col-xxl"> <div class="document-list">
<h1> <h1>
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }} {{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
</h1> </h1>

View File

@ -3,54 +3,54 @@
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} {% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
<div class="item-row"> <div class="item-row">
<div class="item-col" style="width: unset"> <!-- person document or accompanying course document -->
{% if document.object.isPending %} <div class="item-two-col-grid">
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> <div class="title">
{% elseif document.object.isFailure %} {% if document.object.isPending %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> <div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% endif %} {% elseif document.object.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
{% if context == 'person' and accompanyingCourse is defined %} <div class="denomination h2">
<div> {{ document.title|chill_print_or_message("No title") }}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div> </div>
{% elseif context == 'accompanying-period' and person is defined %} {% if document.object.type is not empty %}
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% endif %}
<div> <div>
<span class="badge bg-primary"> <p>{{ document.category.name|localize_translatable_string }}</p>
{{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }}
</span>&nbsp;
</div> </div>
{% if document.object.hasTemplate %}
{% endif %} <div>
<div class="denomination h2"> <p>{{ document.object.template.name|localize_translatable_string }}</p>
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.object.type is not empty %}
<div>
{{ mm.mimeIcon(document.object.type) }}
</div>
{% endif %}
<div>
<p>{{ document.category.name|localize_translatable_string }}</p>
</div>
{% if document.object.hasTemplate %}
<div>
<p>{{ document.object.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
{% if document.date is not null %}
<div class="dates row text-end">
<span>{{ document.date|format_date('short') }}</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if document.date is not null %}
<div class="aside">
<div class="dates row text-end">
<span>{{ document.date|format_date('short') }}</span>
</div>
{% if context == 'person' and accompanyingCourse is defined %}
<div class="text-end">
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ accompanyingCourse.id }}
</span>&nbsp;
</div>
{% elseif context == 'accompanying-period' and person is defined %}
<div class="text-end">
<span class="badge bg-primary">
{{ document.person|chill_entity_render_string }}
</span>&nbsp;
</div>
{% endif %}
</div>
{% endif %}
</div> </div>
</div> </div>
{% if document.description is not empty %} {% if document.description is not empty %}
<div class="item-row"> <div class="item-row">

View File

@ -62,7 +62,15 @@ final readonly class RemoveOldVersionMessageHandler implements MessageHandlerInt
$storedObject = $storedObjectVersion->getStoredObject(); $storedObject = $storedObjectVersion->getStoredObject();
$this->storedObjectManager->delete($storedObjectVersion); if ($this->storedObjectManager->exists($storedObjectVersion)) {
$this->storedObjectManager->delete($storedObjectVersion);
} else {
$this->logger->notice(
self::LOG_PREFIX.'Stored object version does not exists any more.',
['storedObjectVersionName' => $storedObjectVersion->getFilename()],
);
}
// to ensure an immediate deletion // to ensure an immediate deletion
$this->entityManager->remove($storedObjectVersion); $this->entityManager->remove($storedObjectVersion);

View File

@ -44,6 +44,7 @@ class RemoveOldVersionMessageHandlerTest extends TestCase
$entityManager->expects($this->once())->method('clear'); $entityManager->expects($this->once())->method('clear');
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class); $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
$storedObjectManager->expects($this->once())->method('exists')->willReturn(true);
$storedObjectManager->expects($this->once())->method('delete')->with($this->identicalTo($version)); $storedObjectManager->expects($this->once())->method('delete')->with($this->identicalTo($version));
$handler = new RemoveOldVersionMessageHandler($storedObjectVersionRepository, new NullLogger(), $entityManager, $storedObjectManager, new MockClock()); $handler = new RemoveOldVersionMessageHandler($storedObjectVersionRepository, new NullLogger(), $entityManager, $storedObjectManager, new MockClock());
@ -51,6 +52,29 @@ class RemoveOldVersionMessageHandlerTest extends TestCase
$handler(new RemoveOldVersionMessage(1)); $handler(new RemoveOldVersionMessage(1));
} }
public function testInvokeForVersionNotExisting(): void
{
$object = new StoredObject();
$version = $object->registerVersion();
$storedObjectVersionRepository = $this->createMock(StoredObjectVersionRepository::class);
$storedObjectVersionRepository->expects($this->once())->method('find')
->with($this->identicalTo(1))
->willReturn($version);
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects($this->once())->method('remove')->with($this->identicalTo($version));
$entityManager->expects($this->once())->method('flush');
$entityManager->expects($this->once())->method('clear');
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
$storedObjectManager->expects($this->once())->method('exists')->willReturn(false);
$storedObjectManager->expects($this->never())->method('delete')->with($this->identicalTo($version));
$handler = new RemoveOldVersionMessageHandler($storedObjectVersionRepository, new NullLogger(), $entityManager, $storedObjectManager, new MockClock());
$handler(new RemoveOldVersionMessage(1));
}
public function testInvokeWithStoredObjectToDelete(): void public function testInvokeWithStoredObjectToDelete(): void
{ {
$object = new StoredObject(); $object = new StoredObject();
@ -123,6 +147,6 @@ class DummyStoredObjectManager implements StoredObjectManagerInterface
public function exists(StoredObject|StoredObjectVersion $document): bool public function exists(StoredObject|StoredObjectVersion $document): bool
{ {
throw new \RuntimeException(); return true;
} }
} }

View File

@ -99,3 +99,30 @@ CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE: Modifier un document
entity_display_title: entity_display_title:
Document (n°%doc%): "Document (n°%doc%)" Document (n°%doc%): "Document (n°%doc%)"
Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval% Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%
# SIGNATURES
signatures:
yes: Oui
are_you_sure: Êtes-vous sûr·e?
you_are_going_to_sign: Vous allez signer le document
signature_confirmation: Confirmation de la signature
sign: Signer
choose_another_signature: Choisir une autre zone
cancel: Annuler
last_sign_zone: Zone de signature précédente
next_sign_zone: Zone de signature suivante
add_sign_zone: Ajouter une zone de signature
click_on_document: Cliquer sur le document
last_zone: Zone précédente
next_zone: Zone suivante
add_zone: Ajouter une zone
another_zone: Autre zone
electronic_signature_in_progress: Signature électronique en cours...
loading: Chargement...
remove_sign_zone: Enlever la zone
return: Retour
see_all_pages: Voir toutes les pages
all_pages: Toutes les pages
go_to_signature_unique: Aller à la zone de signature

View File

@ -25,7 +25,34 @@ div.flex-table {
div.item-col:last-child { div.item-col:last-child {
display: flex; display: flex;
} }
div.item-two-col-grid {
display: grid;
width: 100%;
justify-content: stretch;
@include media-breakpoint-up(lg) {
grid-template-areas:
"title aside";
grid-template-columns: 1fr minmax(8rem, 1fr);
column-gap: 0.5em;
}
@include media-breakpoint-down(lg) {
grid-template-areas:
"aside"
"title";
}
& > div.title {
grid-area: title;
}
& > div.aside {
grid-area: aside;
}
}
} }
} }
h2, h3, h4, dl, p { h2, h3, h4, dl, p {

View File

@ -26,9 +26,9 @@
trans(THIRDPARTY_CONTACT_OF) trans(THIRDPARTY_CONTACT_OF)
}}</span> }}</span>
<span v-else-if="props.entity.kind === 'company'">{{ <span v-else-if="props.entity.kind === 'company'">{{
trans(THIRDPARTY_A_CONTACT) trans(THIRDPARTY_A_COMPANY)
}}</span> }}</span>
<span v-else>{{ $t("thirdparty.contact") }}</span> <span v-else>{{ trans(THIRDPARTY_A_CONTACT) }}</span>
</template> </template>
</span> </span>
@ -54,6 +54,7 @@ import {
ACCEPTED_USERS, ACCEPTED_USERS,
THIRDPARTY_A_CONTACT, THIRDPARTY_A_CONTACT,
THIRDPARTY_CONTACT_OF, THIRDPARTY_CONTACT_OF,
THIRDPARTY_A_COMPANY,
PERSON, PERSON,
THIRDPARTY, THIRDPARTY,
} from "translator"; } from "translator";

View File

@ -136,6 +136,59 @@
</div> </div>
</div> </div>
<h2>Fix the title in the flex table</h2>
<p>This will fix the layout of the row, with a "title" element, and an aside element. Using <code>css grid</code>, this is quite safe and won't overflow</p>
<xmp>
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-two-col-grid">
<div class="title">This is my title</div>
<div class="aside">Aside value</div>
</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-two-col-grid">
<div class="title">
<div><h3>This is my title, which can be very long and take a lot of place. But it is wrapped successfully, and won't disturb the placement of the aside block</h3></div>
<div>This is a second line</div>
</div>
<div class="aside">Aside value</div>
</div>
</div>
</div>
</div>
</xmp>
<p>will render:</p>
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-two-col-grid">
<div class="title">This is my title</div>
<div class="aside">Aside value</div>
</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-two-col-grid">
<div class="title">
<div><h3>This is my title, which can be very long and take a lot of place. But it is wrapped successfully, and won't disturb the placement of the aside block</h3></div>
<div>This is a second line</div>
</div>
<div class="aside">Aside value</div>
</div>
</div>
</div>
</div>
<h2>Wrap-list</h2> <h2>Wrap-list</h2>
<p>Une liste inline qui s'aligne, puis glisse sous son titre.</p> <p>Une liste inline qui s'aligne, puis glisse sous son titre.</p>
<div class="wrap-list debug"> <div class="wrap-list debug">
@ -392,6 +445,14 @@ Toutes les classes btn-* de bootstrap sont fonctionnelles
</div> </div>
</div> </div>
<div class="row">
<h1>Entity badges</h1>
<span class="badge-accompanying-work-type-simple">Action d'accompagnement</span>
<span class="badge-activity-type-simple">Type d'échange</span>
<span class="badge-calendar-simple">Rendez-vous</span>
</div>
<h1>Badges</h1> <h1>Badges</h1>

View File

@ -25,6 +25,8 @@ use Symfony\Component\Workflow\Transition;
#[AsMessageHandler] #[AsMessageHandler]
final readonly class CancelStaleWorkflowHandler final readonly class CancelStaleWorkflowHandler
{ {
private const LOG_PREFIX = '[CancelStaleWorkflowHandler] ';
public function __construct( public function __construct(
private EntityWorkflowRepository $workflowRepository, private EntityWorkflowRepository $workflowRepository,
private Registry $registry, private Registry $registry,
@ -40,13 +42,13 @@ final readonly class CancelStaleWorkflowHandler
$workflow = $this->workflowRepository->find($message->getWorkflowId()); $workflow = $this->workflowRepository->find($message->getWorkflowId());
if (null === $workflow) { if (null === $workflow) {
$this->logger->alert('Workflow was not found!', [$workflowId]); $this->logger->alert(self::LOG_PREFIX.'Workflow was not found!', ['entityWorkflowId' => $workflowId]);
return; return;
} }
if (false === $workflow->isStaledAt($olderThanDate)) { if (false === $workflow->isStaledAt($olderThanDate)) {
$this->logger->alert('Workflow has transitioned in the meantime.', [$workflowId]); $this->logger->alert(self::LOG_PREFIX.'Workflow has transitioned in the meantime.', ['entityWorkflowId' => $workflowId]);
throw new UnrecoverableMessageHandlingException('the workflow is not staled any more'); throw new UnrecoverableMessageHandlingException('the workflow is not staled any more');
} }
@ -67,14 +69,14 @@ final readonly class CancelStaleWorkflowHandler
'transitionAt' => $this->clock->now(), 'transitionAt' => $this->clock->now(),
'transition' => $transition->getName(), 'transition' => $transition->getName(),
]); ]);
$this->logger->info('EntityWorkflow has been cancelled automatically.', [$workflowId]); $this->logger->info(self::LOG_PREFIX.'EntityWorkflow has been cancelled automatically.', ['entityWorkflowId' => $workflowId]);
$transitionApplied = true; $transitionApplied = true;
break; break;
} }
} }
if (!$transitionApplied) { if (!$transitionApplied) {
$this->logger->error('No valid transition found for EntityWorkflow.', [$workflowId]); $this->logger->error(self::LOG_PREFIX.'No valid transition found for EntityWorkflow.', ['entityWorkflowId' => $workflowId]);
throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId)); throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
} }

View File

@ -26,7 +26,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
{ {
$now = $this->clock->now(); $now = $this->clock->now();
if (null !== $cronJobExecution && $now->sub(new \DateInterval('P1D')) < $cronJobExecution->getLastStart()) { if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
return false; return false;
} }

View File

@ -204,20 +204,25 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle
['date' => 'DESC', 'id' => 'DESC'], ['date' => 'DESC', 'id' => 'DESC'],
); );
$activities = \array_slice($activities, 0, 3);
$works = $this->workRepository->findByAccompanyingPeriod( $works = $this->workRepository->findByAccompanyingPeriod(
$accompanyingCourse, $accompanyingCourse,
['startDate' => 'DESC', 'endDate' => 'DESC'], ['startDate' => 'DESC', 'endDate' => 'DESC'],
3 3
); );
$counters = [
'activities' => count($activities),
'openWorks' => count($accompanyingCourse->getOpenWorks()),
'works' => count($works),
];
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [ return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse, 'accompanyingCourse' => $accompanyingCourse,
'withoutHousehold' => $withoutHousehold, 'withoutHousehold' => $withoutHousehold,
'participationsByHousehold' => $accompanyingCourse->actualParticipationsByHousehold(), 'participationsByHousehold' => $accompanyingCourse->actualParticipationsByHousehold(),
'works' => $works, 'works' => $works,
'activities' => $activities, 'activities' => \array_slice($activities, 0, 3),
'counters' => $counters,
]); ]);
} }

View File

@ -511,6 +511,14 @@ class AccompanyingPeriod implements
return $this->getParticipationsContainsPerson($person)->count() > 0; return $this->getParticipationsContainsPerson($person)->count() > 0;
} }
public function getOpenWorks(): Collection
{
return $this->getWorks()->filter(
static fn (AccompanyingPeriodWork $work): bool => null === $work->getEndDate()
or $work->getEndDate() > new \DateTimeImmutable('today')
);
}
/** /**
* Open a new participation for a person. * Open a new participation for a person.
*/ */

View File

@ -58,13 +58,11 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
private ?int $numberOfChildren = null; private ?int $numberOfChildren = null;
#[Assert\NotNull]
#[Assert\GreaterThanOrEqual(0, groups: ['Default', 'household_composition'])] #[Assert\GreaterThanOrEqual(0, groups: ['Default', 'household_composition'])]
#[Serializer\Groups(['docgen:read'])] #[Serializer\Groups(['docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
private ?int $numberOfDependents = null; private ?int $numberOfDependents = null;
#[Assert\NotNull]
#[Assert\GreaterThanOrEqual(0, groups: ['Default', 'household_composition'])] #[Assert\GreaterThanOrEqual(0, groups: ['Default', 'household_composition'])]
#[Serializer\Groups(['docgen:read'])] #[Serializer\Groups(['docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]

View File

@ -71,7 +71,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
->setExtras(['order' => 30]); ->setExtras(['order' => 30]);
*/ */
$menu->addChild($this->translator->trans('Accompanying Course Comment'), [ $menu->addChild($this->translator->trans('Accompanying Course Comments'), [
'route' => 'chill_person_accompanying_period_comment_list', 'route' => 'chill_person_accompanying_period_comment_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
@ -80,12 +80,15 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
} }
if ($this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $period)) { if ($this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course Action'), [ $menu->addChild($this->translator->trans('Accompanying Course Actions'), [
'route' => 'chill_person_accompanying_period_work_list', 'route' => 'chill_person_accompanying_period_work_list',
'routeParameters' => [ 'routeParameters' => [
'id' => $period->getId(), 'id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 40]); ->setExtras([
'order' => 40,
'counter' => count($period->getOpenWorks()) > 0 ? count($period->getOpenWorks()) : null,
]);
} }
$workflow = $this->registry->get($period, 'accompanying_period_lifecycle'); $workflow = $this->registry->get($period, 'accompanying_period_lifecycle');

View File

@ -304,5 +304,14 @@ div#dashboards {
margin: 0; margin: 0;
} }
} }
div.count-item {
font-size: 3rem;
text-align: center;
}
div.count-item-label {
font-size: 90%;
font-variant: all-small-caps;
text-align: center;
}
} }
} }

View File

@ -20,6 +20,36 @@
} }
} }
.badge-accompanying-work-type-simple {
@extend .badge;
display: inline-block;
margin: 0.2rem 0;
padding-left: 0;
padding-right: 0.5rem;
border-left: 20px groove $orange;
border-radius: $badge-border-radius;
max-width: 100%;
background-color: $gray-100;
color: black;
font-weight: normal;
overflow: hidden;
text-overflow: ellipsis;
text-indent: 5px hanging;
text-align: left;
&::before {
margin-right: 3px;
position: relative;
left: -0.5px;
font-family: ForkAwesome;
content: '\f04b';
color: #e2793d;
}
}
/// AccompanyingCourse Work Pages /// AccompanyingCourse Work Pages
div.accompanying-course-work { div.accompanying-course-work {

View File

@ -204,7 +204,8 @@ export default {
} else if (payload.type === "thirdparty") { } else if (payload.type === "thirdparty") {
body.name = payload.data.text; body.name = payload.data.text;
body.email = payload.data.email; body.email = payload.data.email;
body.telephone = payload.data.phonenumber; body.telephone = payload.data.telephone;
body.telephone2 = payload.data.telephone2;
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
makeFetch( makeFetch(

View File

@ -385,7 +385,8 @@ export default {
} else if (payload.type === "thirdparty") { } else if (payload.type === "thirdparty") {
body.name = payload.data.text; body.name = payload.data.text;
body.email = payload.data.email; body.email = payload.data.email;
body.telephone = payload.data.phonenumber; body.telephone = payload.data.telephone;
body.telephone2 = payload.data.telephone2;
if (payload.data.address) { if (payload.data.address) {
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
} }

View File

@ -194,6 +194,7 @@ export default {
body.name = payload.data.name; body.name = payload.data.name;
body.email = payload.data.email; body.email = payload.data.email;
body.telephone = payload.data.telephone; body.telephone = payload.data.telephone;
body.telephone2 = payload.data.telephone2;
body.address = payload.data.address body.address = payload.data.address
? { id: payload.data.address.address_id } ? { id: payload.data.address.address_id }
: null; : null;

View File

@ -745,7 +745,8 @@ export default {
let body = { type: payload.type }; let body = { type: payload.type };
body.name = payload.data.text; body.name = payload.data.text;
body.email = payload.data.email; body.email = payload.data.email;
body.telephone = payload.data.phonenumber; body.telephone = payload.data.telephone;
body.telephone2 = payload.data.telephone2;
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
makeFetch( makeFetch(
@ -755,7 +756,9 @@ export default {
) )
.then((response) => { .then((response) => {
this.$store.dispatch("updateThirdParty", response); this.$store.dispatch("updateThirdParty", response);
this.$refs.onTheFly.closeModal(); for (let otf of this.$refs.onTheFly) {
otf.closeModal();
}
}) })
.catch((error) => { .catch((error) => {
if (error.name === "ValidationException") { if (error.name === "ValidationException") {

View File

@ -201,7 +201,7 @@
{% endif %} {% endif %}
{% if accompanyingCourse.step != 'DRAFT' %} {% if accompanyingCourse.step != 'DRAFT' %}
<div class="mbloc col col-sm-6 col-lg-4"> <div class="mbloc col col-sm-6 col-lg-8 col-xxl-4">
<div class="notification-counter"> <div class="notification-counter">
<h4 class="item-key">{{ 'notification.Notifications'|trans }}</h4> <h4 class="item-key">{{ 'notification.Notifications'|trans }}</h4>
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %} {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
@ -238,6 +238,31 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if counters.activities > 0 %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="count-activities">
<div class="count-item">{{ counters.activities }}</div>
<div class="count-item-label">
{% if counters.activities == 1 %}
{{ 'Activity'|trans }}
{% else %}
{{ 'Activities'|trans }}
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if counters.works > 0 %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="count-works">
<div class="count-item">{{ counters.openWorks }} / {{ counters.works }}</div>
<div class="count-item-label">{{ 'accompanying_course_work.On-going works over total'|trans }}</div>
</div>
</div>
{% endif %}
</div> </div>
<div class="social-actions my-4"> <div class="social-actions my-4">

View File

@ -5,44 +5,49 @@
{% set w = document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork %} {% set w = document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork %}
<div class="item-row"> <div class="item-row">
<div class="item-col" style="width: unset"> <!-- evaluation document -->
{% if document.storedObject.isPending %} <div class="item-two-col-grid" style="width: unset">
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> <div class="title">
{% elseif document.storedObject.isFailure %} {% if document.storedObject.isPending %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> <div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% endif %} {% elseif document.storedObject.isFailure %}
<div> <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% if context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ w.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %} {% endif %}
<div class="badge-accompanying-work-type">
<span class="title_label"></span>
<span class="title_action">{{ w.socialAction|chill_entity_render_string }} > {{ document.accompanyingPeriodWorkEvaluation.evaluation.title|localize_translatable_string }}</span>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.type is not empty %}
<div> <div>
{{ mm.mimeIcon(document.storedObject.type) }} <div>
<div class="badge-accompanying-work-type-simple">
{{ w.socialAction|chill_entity_render_string }} > {{ document.accompanyingPeriodWorkEvaluation.evaluation.title|localize_translatable_string }}
</div>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.storedObject.type is not empty %}
<div>
{{ mm.mimeIcon(document.storedObject.type) }}
</div>
{% endif %}
{% if document.storedObject.hasTemplate %}
<div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
{% if document.storedObject.createdAt is not null %}
<div class="aside">
<div class="dates row text-end">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</div>
{% if context == 'person' %}
<div class="text-end">
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ w.accompanyingPeriod.id }}
</span>&nbsp;
</div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if document.storedObject.hasTemplate %}
<div>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -804,7 +804,7 @@ person_admin:
# specific to accompanying period # specific to accompanying period
accompanying_period: accompanying_period:
deleted: Parcours d'accompagnment supprimé deleted: Parcours d'accompagnement supprimé
dates: Période dates: Période
dates_from_%opening_date%: Ouvert depuis le %opening_date% dates_from_%opening_date%: Ouvert depuis le %opening_date%
dates_from_%opening_date%_to_%closing_date%: Ouvert du %opening_date% au %closing_date% dates_from_%opening_date%_to_%closing_date%: Ouvert du %opening_date% au %closing_date%
@ -843,6 +843,7 @@ accompanying_course:
administrative_location: Localisation administrative administrative_location: Localisation administrative
comment is pinned: Le commentaire est épinglé comment is pinned: Le commentaire est épinglé
comment is unpinned: Le commentaire est désépinglé comment is unpinned: Le commentaire est désépinglé
show: Montrer show: Montrer
hide: Masquer hide: Masquer
closed periods: parcours clôturés closed periods: parcours clôturés
@ -851,6 +852,7 @@ Social work configuration: Gestion des actions d'accompagnement social
# Accompanying Course comments # Accompanying Course comments
Accompanying Course Comment: Commentaire Accompanying Course Comment: Commentaire
Accompanying Course Comments: Commentaires
Accompanying Course Comment list: Commentaires du parcours Accompanying Course Comment list: Commentaires du parcours
pinned: épinglé pinned: épinglé
Pin comment: Épingler Pin comment: Épingler
@ -919,6 +921,7 @@ accompanying_course_work:
date_filter: Filtrer par date date_filter: Filtrer par date
types_filter: Filtrer par type d'action types_filter: Filtrer par type d'action
user_filter: Filtrer par intervenant user_filter: Filtrer par intervenant
On-going works over total: Actions en cours / Actions du parcours
# #

View File

@ -65,6 +65,7 @@ class ThirdpartyCSVExportController extends AbstractController
'Name', 'Name',
'Profession', 'Profession',
'Telephone', 'Telephone',
'Telephone2',
'Email', 'Email',
'Address', 'Address',
'Comment', 'Comment',
@ -76,6 +77,7 @@ class ThirdpartyCSVExportController extends AbstractController
'Contact name', 'Contact name',
'Contact firstname', 'Contact firstname',
'Contact phone', 'Contact phone',
'Contact phone2',
'Contact email', 'Contact email',
'Contact address', 'Contact address',
'Contact profession', 'Contact profession',

View File

@ -209,6 +209,11 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
#[PhonenumberConstraint(type: 'any')] #[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $telephone = null; private ?PhoneNumber $telephone = null;
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'telephone2', type: 'phone_number', nullable: true)]
#[PhonenumberConstraint(type: 'any')]
private ?PhoneNumber $telephone2 = null;
#[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)] #[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)]
private ?array $thirdPartyTypes = []; private ?array $thirdPartyTypes = [];
@ -429,6 +434,11 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
return $this->telephone; return $this->telephone;
} }
public function getTelephone2(): ?PhoneNumber
{
return $this->telephone2;
}
/** /**
* Get type. * Get type.
*/ */
@ -712,6 +722,13 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
return $this; return $this;
} }
public function setTelephone2(?PhoneNumber $telephone2 = null): self
{
$this->telephone2 = $telephone2;
return $this;
}
/** /**
* Set type. * Set type.
* *

View File

@ -59,6 +59,10 @@ class ThirdPartyType extends AbstractType
'label' => 'Phonenumber', 'label' => 'Phonenumber',
'required' => false, 'required' => false,
]) ])
->add('telephone2', ChillPhoneNumberType::class, [
'label' => 'telephone2',
'required' => false,
])
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'required' => false, 'required' => false,
]) ])

View File

@ -42,6 +42,7 @@ class ThirdPartyRepository implements ObjectRepository
parent.name AS name, parent.name AS name,
parent.profession AS profession, parent.profession AS profession,
parent.telephone AS telephone, parent.telephone AS telephone,
parent.telephone2 AS telephone2,
parent.email AS email, parent.email AS email,
CONCAT_WS(' ', parent_address.street, parent_address.streetnumber, parent_postal.code, parent_postal.label) AS address, CONCAT_WS(' ', parent_address.street, parent_address.streetnumber, parent_postal.code, parent_postal.label) AS address,
parent.comment AS comment, parent.comment AS comment,
@ -55,6 +56,7 @@ class ThirdPartyRepository implements ObjectRepository
contact.name AS contact_name, contact.name AS contact_name,
contact.firstname AS contact_firstname, contact.firstname AS contact_firstname,
contact.telephone AS contact_phone, contact.telephone AS contact_phone,
contact.telephone2 AS contact_phone2,
contact.email AS contact_email, contact.email AS contact_email,
contact.profession AS contact_profession, contact.profession AS contact_profession,
CONCAT_WS(' ', contact_address.street, contact_address.streetnumber, contact_postal.code, contact_postal.label) AS contact_address CONCAT_WS(' ', contact_address.street, contact_address.streetnumber, contact_postal.code, contact_postal.label) AS contact_address

View File

@ -91,6 +91,18 @@
}}</a }}</a
> >
</li> </li>
<li v-if="thirdparty.telephone2">
<i class="fa fa-li fa-mobile" />
<a
:href="
'tel: ' +
thirdparty.telephone2
"
>{{
thirdparty.telephone2
}}</a
>
</li>
<li v-if="thirdparty.email"> <li v-if="thirdparty.email">
<i <i
class="fa fa-li fa-envelope-o" class="fa fa-li fa-envelope-o"
@ -121,6 +133,12 @@
thirdparty.telephone thirdparty.telephone
}}</a> }}</a>
</li> </li>
<li v-if="thirdparty.telephone2">
<i class="fa fa-li fa-mobile" />
<a :href="'tel: ' + thirdparty.telephone2"
>{{ thirdparty.telephone2 }}
</a>
</li>
<li v-if="thirdparty.email"> <li v-if="thirdparty.email">
<i class="fa fa-li fa-envelope-o" /> <i class="fa fa-li fa-envelope-o" />
<a :href="'mailto: ' + thirdparty.email">{{ <a :href="'mailto: ' + thirdparty.email">{{

View File

@ -223,6 +223,19 @@
/> />
</div> </div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber2"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdparty.telephone2"
:placeholder="$t('thirdparty.phonenumber2')"
:aria-label="$t('thirdparty.phonenumber2')"
aria-describedby="phonenumber2"
/>
</div>
<div v-if="parent"> <div v-if="parent">
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="comment" <span class="input-group-text" id="comment"
@ -263,6 +276,7 @@ export default {
firstname: "", firstname: "",
name: "", name: "",
telephone: "", telephone: "",
telephone2: "",
civility: null, civility: null,
profession: "", profession: "",
}, },
@ -368,9 +382,11 @@ export default {
addQueryItem(field, queryItem) { addQueryItem(field, queryItem) {
switch (field) { switch (field) {
case "name": case "name":
this.thirdparty.name if (this.thirdparty.name) {
? (this.thirdparty.name += ` ${queryItem}`) this.thirdparty.name += ` ${queryItem}`;
: (this.thirdparty.name = queryItem); } else {
this.thirdparty.name = queryItem;
}
break; break;
case "firstName": case "firstName":
this.thirdparty.firstname = queryItem; this.thirdparty.firstname = queryItem;

View File

@ -6,6 +6,7 @@ const thirdpartyMessages = {
name: "Dénomination", name: "Dénomination",
email: "Courriel", email: "Courriel",
phonenumber: "Téléphone", phonenumber: "Téléphone",
phonenumber2: "Autre numéro de téléphone",
comment: "Commentaire", comment: "Commentaire",
profession: "Qualité", profession: "Qualité",
civility: "Civilité", civility: "Civilité",

View File

@ -115,6 +115,10 @@
{% else %} {% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span> <span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %} {% endif %}
{% if thirdparty.telephone2 is not null %}
{% if thirdparty.telephone is not null %}, {% endif %}
<a href="{{ 'tel:' ~ thirdparty.telephone2|phone_number_format('E164') }}">{{ thirdparty.telephone2|chill_format_phonenumber }}</a>
{% endif %}
</li> </li>
<li><i class="fa fa-li fa-envelope-o"></i> <li><i class="fa fa-li fa-envelope-o"></i>
<a href="{{ 'mailto:' ~ thirdparty.email }}"> <a href="{{ 'mailto:' ~ thirdparty.email }}">
@ -135,8 +139,14 @@
}) }} }) }}
</li> </li>
<li><i class="fa fa-li fa-phone"></i> <li><i class="fa fa-li fa-phone"></i>
{% if thirdparty.telephone %} {% if thirdparty.telephone or thirdparty.telephone2 %}
<a href="{{ 'tel:' ~ thirdparty.telephone|phone_number_format('E164') }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a> {% if thirdparty.telephone is not null %}
<a href="{{ 'tel:' ~ thirdparty.telephone|phone_number_format('E164') }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a>
{% endif %}
{% if thirdparty.telephone2 is not null %}
{% if thirdparty.telephone is not null %}, {% endif %}
<a href="{{ 'tel:' ~ thirdparty.telephone2|phone_number_format('E164') }}">{{ thirdparty.telephone2|chill_format_phonenumber }}</a>
{% endif %}
{% else %} {% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span> <span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %} {% endif %}

View File

@ -22,6 +22,7 @@
{{ form_row(form.typesAndCategories) }} {{ form_row(form.typesAndCategories) }}
{{ form_row(form.telephone) }} {{ form_row(form.telephone) }}
{{ form_row(form.telephone2) }}
{{ form_row(form.email) }} {{ form_row(form.email) }}
{% if form.contactDataAnonymous is defined %} {% if form.contactDataAnonymous is defined %}

View File

@ -24,17 +24,24 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-5 mb-3"> <div class="form-group col-md-6 mb-3">
{{ form_widget(form.telephone) }} {{ form_widget(form.telephone) }}
{{ form_errors(form.telephone) }} {{ form_errors(form.telephone) }}
{{ form_label(form.telephone) }} {{ form_label(form.telephone) }}
</div> </div>
<div class="form-group col-md-5 mb-3"> <div class="form-group col-md-6 mb-3">
{{ form_widget(form.telephone2) }}
{{ form_errors(form.telephone2) }}
{{ form_label(form.telephone2) }}
</div>
</div>
<div class="row">
<div class="form-group col-md-6 mb-3">
{{ form_widget(form.email) }} {{ form_widget(form.email) }}
{{ form_errors(form.email) }} {{ form_errors(form.email) }}
{{ form_label(form.email) }} {{ form_label(form.email) }}
</div> </div>
<div class="form-group col-md-2 mb-3"> <div class="form-group col-md-6 mb-3">
{{ form_widget(form.contactDataAnonymous) }} {{ form_widget(form.contactDataAnonymous) }}
{{ form_label(form.contactDataAnonymous) }} {{ form_label(form.contactDataAnonymous) }}
{{ form_errors(form.contactDataAnonymous) }} {{ form_errors(form.contactDataAnonymous) }}

View File

@ -76,6 +76,18 @@
{% endif %} {% endif %}
</dd> </dd>
<dt>{{ 'Phonenumber2'|trans }}</dt>
<dd>
{% if thirdParty.telephone2 == null %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% else %}
<a href="{{ 'tel:' ~ thirdParty.telephone2|phone_number_format('E164') }}">
{{ thirdParty.telephone2|chill_format_phonenumber }}
</a>
{% endif %}
</dd>
<dt>{{ 'email'|trans }}<dt> <dt>{{ 'email'|trans }}<dt>
<dd> <dd>
{% if thirdParty.email == null %} {% if thirdParty.email == null %}

View File

@ -55,6 +55,7 @@ class ThirdPartyNormalizer implements NormalizerAwareInterface, NormalizerInterf
'profession' => $this->normalizer->normalize($thirdParty->getProfession(), $format, $context), 'profession' => $this->normalizer->normalize($thirdParty->getProfession(), $format, $context),
'address' => $this->normalizer->normalize($thirdParty->getAddress(), $format, ['address_rendering' => 'short']), 'address' => $this->normalizer->normalize($thirdParty->getAddress(), $format, ['address_rendering' => 'short']),
'telephone' => $this->normalizer->normalize($thirdParty->getTelephone(), $format, $context), 'telephone' => $this->normalizer->normalize($thirdParty->getTelephone(), $format, $context),
'telephone2' => $this->normalizer->normalize($thirdParty->getTelephone2(), $format, $context),
'email' => $thirdParty->getEmail(), 'email' => $thirdParty->getEmail(),
'isChild' => $thirdParty->isChild(), 'isChild' => $thirdParty->isChild(),
'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context), 'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context),

View File

@ -28,6 +28,8 @@ components:
type: string type: string
telephone: telephone:
type: string type: string
telephone2:
type: string
address: address:
$ref: "#/components/schemas/Address" $ref: "#/components/schemas/Address"
Address: Address:

View File

@ -0,0 +1,34 @@
<?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\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250325085950 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a second telephone number to ThirdParty';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party ADD telephone2 VARCHAR(35) DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN chill_3party.third_party.telephone2 IS \'(DC2Type:phone_number)\'');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party DROP telephone2');
}
}

View File

@ -4,6 +4,7 @@ third parties: tiers
firstname: Prénom firstname: Prénom
name: Nom name: Nom
telephone: Téléphone telephone: Téléphone
telephone2: Autre numéro de téléphone
adress: Adresse adress: Adresse
email: Courriel email: Courriel
comment: Commentaire comment: Commentaire
@ -39,7 +40,7 @@ thirdparty.A contact: Une personne physique
thirdparty.contact: Personne physique thirdparty.contact: Personne physique
thirdparty.Contact of: Contact de thirdparty.Contact of: Contact de
thirdparty.a_company_explanation: >- thirdparty.a_company_explanation: >-
Les personnes morales peuvent compter un ou plusieurs contacts, interne à l'instution. Il est également possible de Les personnes morales peuvent compter un ou plusieurs contacts, interne à l'institution. Il est également possible de
leur associer un acronyme, et le nom d'un service. leur associer un acronyme, et le nom d'un service.
thirdparty.a_contact_explanation: >- thirdparty.a_contact_explanation: >-
Les personnes physiques ne disposent pas d'acronyme, de service, ou de contacts sous-jacents. Il est possible de leur Les personnes physiques ne disposent pas d'acronyme, de service, ou de contacts sous-jacents. Il est possible de leur
@ -149,6 +150,8 @@ Contact id: Identifiant du contact
Contact name: Nom du contact Contact name: Nom du contact
Contact firstname: Prénom du contact Contact firstname: Prénom du contact
Contact phone: Téléphone du contact Contact phone: Téléphone du contact
Contact phone2: Autre téléphone du contact
Telephone2: Autre téléphone
Contact email: Courrier électronique du contact Contact email: Courrier électronique du contact
Contact address: Adresse du contact Contact address: Adresse du contact
Contact profession: Profession du contact Contact profession: Profession du contact