Compare commits

...

69 Commits

Author SHA1 Message Date
b5c9e65986 Merge branch 'refs/heads/master' into 321-text-editor
# Conflicts:
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/editor_config.ts
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/index.ts
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/WriteComment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/PersonComment.vue
#	yarn.lock
2025-05-21 20:03:14 +02:00
8b2af35e97 Fix typo 2025-05-21 17:57:35 +02:00
dc44c46667 Fix SocialActionCSVExporterTest.php 2025-05-21 09:29:25 +02:00
ba571c1a69 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-05-20 10:04:53 +02:00
6a364705f2 Git add desactivation date for social issue csv export 2025-05-20 09:57:07 +02:00
b6d454691a Git add desactivation date for social action csv export 2025-05-20 09:38:39 +02:00
6d7a6932a9 Merge branch '365-correct-activities-works-counters' into 'master'
#365 correct works counter in acc course summary

Closes #365

See merge request Chill-Projet/chill-bundles!826
2025-05-16 14:40:20 +00:00
juminet
2faf194b15 #365 correct works counter in acc course summary 2025-05-16 14:40:19 +00:00
f207599d86 Merge branch '352-remove-wopi-link-module' into 'master'
Resolve "Code mort: module wopi-link semble inutilisé"

Closes #352

See merge request Chill-Projet/chill-bundles!824
2025-05-16 08:33:57 +00:00
b0959f8cc5 Resolve "Code mort: module wopi-link semble inutilisé" 2025-05-16 08:33:56 +00:00
4c5dee5f0a Fix pipeline 2025-05-14 17:31:48 +02:00
f6c98aa0d5 Add missing translation for user_group.no_user_groups 2025-05-14 14:53:50 +02:00
6d13d184d5 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-05-14 13:57:22 +02:00
af36eccfaf Allow more characters for maritalstatus id 2025-05-14 13:56:36 +02:00
483a20a43f Merge branch '374-task-default-filters' into 'master'
Resolve "Module tâche: enlever les filtres par défaut"

Closes #374

See merge request Chill-Projet/chill-bundles!819
2025-05-13 08:59:01 +00:00
6d8e2ad825 Resolve "Module tâche: enlever les filtres par défaut" 2025-05-13 08:59:01 +00:00
86388a63a8 Merge branch '381-uncoherent-display-persons-acc-course-works' into 'master'
381 display previous person participation in acc course work

Closes #381

See merge request Chill-Projet/chill-bundles!823
2025-05-13 08:45:38 +00:00
juminet
5ea55ebfe5 381 display previous person participation in acc course work 2025-05-13 08:45:38 +00:00
f97dc8f931 Merge branch '377-document-file-name' into 'master'
377 - add the document filename to document title when uploading a new document

Closes #377

See merge request Chill-Projet/chill-bundles!821
2025-05-13 08:30:31 +00:00
juminet
a9c3aab528 377 - add the document filename to document title when uploading a new document 2025-05-13 08:30:30 +00:00
1181377bd6 Merge branch '376-typeerror-doc-history' into 'master'
#376 prevent typerror in doc-history + improve display of document history

Closes #376

See merge request Chill-Projet/chill-bundles!820
2025-05-13 07:50:53 +00:00
juminet
2275b7c560 #376 prevent typerror in doc-history + improve display of document history 2025-05-13 07:50:52 +00:00
4a8d298ae5 Fix typing error for the display of text in calendar events 2025-05-05 10:42:04 +02:00
3e7f03d331 Add possibility to cancel a workflow if there is a pending signature [ci-skip] 2025-04-17 14:23:32 +02:00
b830952b9e Release v3.11.0 2025-04-17 14:22:35 +02:00
ad17313c61 Merge branch '380-remove-not-null-constraint-household-composition' into 'master'
Remove "not null" validation on HouseholdComposition properties

Closes #380

See merge request Chill-Projet/chill-bundles!818
2025-04-17 09:03:15 +00:00
620515ad15 Remove "not null" validation on HouseholdComposition properties
This change removes the "not null" constraint on specific properties in the HouseholdComposition entity to allow null values. The adjustment addresses Issue #380 and ensures better flexibility without impacting the schema.
2025-04-17 10:56:45 +02:00
50c377ee22 Merge branch 'fix/fix-stored-object-version-not-delete-when-not-exists-on-disk' into 'master'
Fix error when cleaning non-existent stored object versions

See merge request Chill-Projet/chill-bundles!816
2025-04-17 08:36:36 +00:00
cc7e7a90ee Merge branch 'fix/cancel-stale-workflow-handle-fails-silently' into 'master'
Add consistent LOG_PREFIX and key to CancelStaleWorkflowHandler logs

See merge request Chill-Projet/chill-bundles!817
2025-04-16 19:10:37 +00:00
1d4ef19051 Add key to log messages context and add log prefix to enhance CancelStaleWorkflowHandler messages
Introduced a consistent LOG_PREFIX to all log messages in CancelStaleWorkflowHandler. This ensures clearer contextual identification in logs and improves traceability when debugging or monitoring workflow actions.
2025-04-16 21:03:29 +02:00
8337a724d1 Fix error when cleaning non-existent stored object versions
Prevent the `RemoveOldVersionMessageHandler` from throwing errors when the stored object version is missing on disk. Introduced a check to log a notice instead of attempting deletion in such cases and added corresponding test coverage.
2025-04-16 16:26:02 +02:00
8ca377d5d4 Merge branch 'button-signature-zone' into 'master'
Add button unique signature zone

See merge request Chill-Projet/chill-bundles!812
2025-04-15 13:09:54 +00:00
224e0bae43 Add button unique signature zone 2025-04-15 13:09:54 +00:00
3aa4fac80d Merge branch '364-tel2-third-party' into 'master'
Adding a second phone number to thirdparty entity

Closes #364

See merge request Chill-Projet/chill-bundles!810
2025-04-15 12:59:58 +00:00
juminet
a7517eb647 Adding a second phone number to thirdparty entity 2025-04-15 12:59:57 +00:00
e278e636e0 Merge branch '365-add-activities-works-counter' into 'master'
#365 Add works and activities counter

Closes #365

See merge request Chill-Projet/chill-bundles!809
2025-04-14 09:34:02 +00:00
juminet
40e373a9c7 #365 Add works and activities counter 2025-04-14 09:34:02 +00:00
1c1f418b18 Merge branch 'fix/acc-period-step-change-shorten-elapsed' into 'master'
Adjust cronjob interval to ensure daily execution

See merge request Chill-Projet/chill-bundles!814
2025-04-10 13:49:11 +00:00
bf0e14b43a Merge branch '102-liste-des-document-titre-long' into 'master'
Fix graphical bug in document list with title overflowing the frame, and add new classes to display title and aside in flex-table (DX + Fix)

Closes #102 and #22

See merge request Chill-Projet/chill-bundles!815
2025-04-10 13:47:40 +00:00
203a098054 Refactor document row layouts to use CSS grid
Replaced the old 'item-col' structure with a 'item-two-col-grid' layout across multiple templates, improving consistency and responsiveness. Introduced CSS grid styles ensuring proper alignment and wrapping of titles and aside elements in different viewport sizes. This enhances the overall readability and maintainability of the views.
2025-04-10 15:41:06 +02:00
d58acff541 Add css layout for badges for accompanying period work, activity and calendar 2025-04-10 15:41:06 +02:00
5858e05a42 Replace grid in person_list document to fit the whole width of the page 2025-04-10 15:31:06 +02:00
b9b4fafe14 Adjust cronjob interval to ensure daily execution
The interval for `AccompanyingPeriodStepChangeCronjob` was reduced from 24 hours to 23 hours and 45 minutes. This change guarantees at least one execution per day, addressing potential issues with timing overlaps.
2025-04-09 21:30:41 +02:00
2dcce7b826 Remove dumps 2025-02-25 15:29:45 +01:00
dcd1777a70 Pass content of the textEditor to the symfony form 2025-02-17 16:25:17 +01:00
a6eb28175a Revert backend to original state and load commentEditor vue app anytime CommentType form field is used 2025-02-17 14:13:07 +01:00
7d78512823 Change symfony form back to original 2025-02-17 13:11:03 +01:00
d0cd4792d6 Fix dynamicEntityPicker: declare variables 2025-02-13 15:02:06 +01:00
6d196ead94 Merge branch '321-text-editor' of https://gitlab.com/Chill-Projet/chill-bundles into 321-text-editor 2025-02-13 14:49:51 +01:00
4047d5fd5b WIP Allow for comment content to be submitted to backend 2025-02-13 14:49:33 +01:00
9aac80d834 Change logic to allow as many comment vue apps to be charged on same page as needed 2025-02-13 14:49:33 +01:00
7560dc57c6 Remove comment editor component from Activity vue app 2025-02-13 14:49:33 +01:00
10314845f6 Turn component into a small vue app and add public private comment logic 2025-02-13 14:49:33 +01:00
9b84bc4d69 Use update ckeditor5 2025-02-13 14:49:33 +01:00
a2fcf039be Refactor CKEditor integration across application
Replaced `ClassicEditor` with a consistent `classicEditor` and added centralized editor configuration using `editorConfig`. Simplified webpack configuration by removing CKEditor-specific setups and dependencies, improving maintainability.
2025-02-13 14:49:33 +01:00
b4d887a372 upgrade ckeditor 2025-02-13 14:49:32 +01:00
0aaa7122da upgrade ckeditor/vue 2025-02-13 14:49:32 +01:00
1bc7f85874 Implement localStorage and toggling of simple/rich text editor 2025-02-13 14:49:32 +01:00
1d2fd000aa Setup new component for text editor 2025-02-13 14:49:32 +01:00
506df432b0 WIP Allow for comment content to be submitted to backend 2025-02-06 16:07:40 +01:00
c32c18b0e2 Change logic to allow as many comment vue apps to be charged on same page as needed 2025-02-06 15:10:49 +01:00
321d569ee9 Remove comment editor component from Activity vue app 2025-02-06 14:32:22 +01:00
cd40eb3932 Turn component into a small vue app and add public private comment logic 2025-02-06 12:46:19 +01:00
f0f2531fa3 Use update ckeditor5 2025-02-06 10:33:32 +01:00
183a220e7b Refactor CKEditor integration across application
Replaced `ClassicEditor` with a consistent `classicEditor` and added centralized editor configuration using `editorConfig`. Simplified webpack configuration by removing CKEditor-specific setups and dependencies, improving maintainability.
2025-02-06 10:33:32 +01:00
9df127a82c upgrade ckeditor 2025-02-06 10:33:32 +01:00
04a1412562 upgrade ckeditor/vue 2025-02-06 10:33:32 +01:00
3aef0a185e Implement localStorage and toggling of simple/rich text editor 2025-02-06 10:33:32 +01:00
578bce31b9 Setup new component for text editor 2025-02-06 10:33:32 +01:00
95 changed files with 1214 additions and 602 deletions

View File

@@ -0,0 +1,6 @@
kind: DX
body: Remove dead code for wopi-link module
time: 2025-04-30T14:45:50.406111606+02:00
custom:
Issue: "352"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Feature
body: Add the document file name to the document title when a user upload a document,
unless there is already a document title.
time: 2025-04-24T14:22:11.800975422+02:00
custom:
Issue: "377"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Add desactivation date for social action and issue csv export
time: 2025-05-20T09:56:28.108941934+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Fixed
body: trying to prevent bug of typeerror in doc-history + improved display of document
history
time: 2025-04-24T13:39:43.878468232+02:00
custom:
Issue: "376"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Fixed
body: Display previous participation in acc course work even if the person has left
the acc course
time: 2025-04-24T16:37:46.970203594+02:00
custom:
Issue: "381"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Fix display of text in calendar events
time: 2025-05-05T10:27:15.461493066+02:00
custom:
Issue: "372"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Add missing translation for user_group.no_user_groups
time: 2025-05-14T14:53:39.53927329+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
time: 2025-04-23T17:26:24.45777387+02:00
custom:
Issue: "374"
SchemaChange: No schema change

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
- attenteMiseEnForme
- attenteValidationMiseEnForme
- attenteSignature
- attenteVisa
- postSignature
- attenteTraitement

View File

@@ -11,6 +11,7 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -23,22 +24,30 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
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)
{
$period = $parameters['accompanyingCourse'];
$activities = $this->managerRegistry->getManager()->getRepository(Activity::class)->findBy(
['accompanyingPeriod' => $period]
);
if (
AccompanyingPeriod::STEP_DRAFT !== $period->getStep()
&& $this->security->isGranted(ActivityVoter::SEE, $period)
) {
$menu->addChild($this->translator->trans('Activity'), [
$menu->addChild($this->translator->trans('Activities'), [
'route' => 'chill_activity_activity_list',
'routeParameters' => [
'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;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Person;
@@ -23,13 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
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)
{
/** @var Person $person */
$person = $parameters['person'];
$count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE);
if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
$menu->addChild(
$this->translator->trans('Activities'),
@@ -38,7 +46,7 @@ final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface
'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;
}
}
.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

@@ -11,7 +11,7 @@ import Location from "./components/Location.vue";
export default {
name: "App",
props: ["hasSocialIssues", "hasLocation", "hasPerson"],
props: ["hasSocialIssues", "hasLocation", "hasPerson", "isSimpleEditor"],
components: {
ConcernedGroups,
SocialIssuesAcc,

View File

@@ -14,18 +14,21 @@ const i18n = _createI18n(activityMessages);
const hasSocialIssues = document.querySelector("#social-issues-acc") !== null;
const hasLocation = document.querySelector("#location") !== null;
const hasPerson = document.querySelector("#add-persons") !== null;
const isSimpleEditor = true;
const app = createApp({
template: `<app
:hasSocialIssues="hasSocialIssues"
:hasLocation="hasLocation"
:hasPerson="hasPerson"
:isSimpleEditor = "isSimpleEditor"
></app>`,
data() {
return {
hasSocialIssues,
hasLocation,
hasPerson,
isSimpleEditor
};
},
})

View File

@@ -126,4 +126,4 @@
{% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% endblock %}

View File

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

View File

@@ -1,5 +1,6 @@
@import '~ChillPersonAssets/chill/scss/mixins.scss';
@import '~ChillMainAssets/module/bootstrap/shared';
@import '~ChillPersonAssets/chill/scss/mixins.scss';
@import 'bootstrap/scss/_badge.scss';
.badge-calendar {
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 {
display: inline-block;;
display: inline-block;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);

View File

@@ -96,23 +96,23 @@
</div>
</div>
<FullCalendar :options="calendarOptions" ref="calendarRef">
<template v-slot:eventContent="{ arg }: { arg: { event: EventApi } }">
<span :class="eventClasses(arg.event)">
<b v-if="arg.event.extendedProps.is === 'remote'">{{
arg.event.title
<template v-slot:eventContent="{ event }">
<span :class="eventClasses(event)">
<b v-if="event.extendedProps.is === 'remote'">{{
event.title
}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'"
>{{ arg.event.startStr }} -
{{ arg.event.extendedProps.locationName }}</b
<b v-else-if="event.extendedProps.is === 'range'"
>{{ formatDate(event.startStr) }} -
{{ event.extendedProps.locationName }}</b
>
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
arg.event.title
<b v-else-if="event.extendedProps.is === 'local'">{{
event.title
}}</b>
<b v-else>no 'is'</b>
<a
v-if="arg.event.extendedProps.is === 'range'"
v-if="event.extendedProps.is === 'range'"
class="fa fa-fw fa-times delete"
@click.prevent="onClickDelete(arg.event)"
@click.prevent="onClickDelete(event)"
>
</a>
</span>
@@ -221,13 +221,12 @@ import type {
DatesSetArg,
EventInput,
} from "@fullcalendar/core";
import { reactive, computed, ref, onMounted } from "vue";
import { computed, ref, onMounted } from "vue";
import { useStore } from "vuex";
import { key } from "./store";
import FullCalendar from "@fullcalendar/vue3";
import frLocale from "@fullcalendar/core/locales/fr";
import interactionPlugin, {
DropArg,
EventResizeDoneArg,
} from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
@@ -237,19 +236,13 @@ import {
EventDropArg,
EventClickArg,
} from "@fullcalendar/core";
import {
dateToISO,
ISOToDate,
} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
import { dateToISO, ISOToDate } from "ChillMainAssets/chill/js/date";
import VueMultiselect from "vue-multiselect";
import { Location } from "../../../../../ChillMainBundle/Resources/public/types";
import { Location } from "ChillMainAssets/types";
import EditLocation from "./Components/EditLocation.vue";
import { useI18n } from "vue-i18n";
const store = useStore(key);
const { t } = useI18n();
const showWeekends = ref(false);
const slotDuration = ref("00:15:00");
const slotMinTime = ref("09:00:00");
@@ -301,6 +294,11 @@ const nextWeeks = computed((): Weeks[] =>
}),
);
const formatDate = (datetime: string) => {
console.log(typeof datetime);
return ISOToDate(datetime);
};
const baseOptions = ref<CalendarOptions>({
locale: frLocale,
plugins: [interactionPlugin, timeGridPlugin],
@@ -353,7 +351,7 @@ const pickedLocation = computed<Location | null>({
* return the show classes for the event
* @param arg
*/
const eventClasses = function (arg: EventApi): object {
const eventClasses = function (): object {
return { calendarRangeItems: true };
};
@@ -431,7 +429,6 @@ function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
if (payload.event.extendedProps.is !== "range") {
return;
}
const changedEvent = payload.event;
store.dispatch("calendarRanges/patchRangeTime", {
calendarRangeId: payload.event.extendedProps.calendarRangeId,

View File

@@ -6,50 +6,48 @@
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<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;
<div class="item-two-col-grid">
<div class="title">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% 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>
<p>{{ document.storedObject.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<span class="badge-calendar-simple">
{{ '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">
<span>{{ document.storedObject.createdAt|format_date('short') }}</span>
</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>

View File

@@ -10,6 +10,9 @@ const startApp = (
collectionEntry: null | HTMLLIElement,
): void => {
console.log("app started", divElement);
const inputTitle = collectionEntry?.querySelector("input[type='text']");
const input_stored_object: HTMLInputElement | null =
divElement.querySelector("input[data-stored-object]");
if (null === input_stored_object) {
@@ -26,9 +29,10 @@ const startApp = (
const app = createApp({
template:
'<drop-file-widget :existingDoc="this.$data.existingDoc" :allowRemove="true" @addDocument="this.addDocument" @removeDocument="removeDocument"></drop-file-widget>',
data(vm) {
data() {
return {
existingDoc: existingDoc,
inputTitle: inputTitle,
};
},
components: {
@@ -38,10 +42,13 @@ const startApp = (
addDocument: function ({
stored_object,
stored_object_version,
file_name,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
}): void {
stored_object.title = file_name;
console.log("object added", stored_object);
console.log("version added", stored_object_version);
this.$data.existingDoc = stored_object;
@@ -49,6 +56,11 @@ const startApp = (
input_stored_object.value = JSON.stringify(
this.$data.existingDoc,
);
if (this.$data.inputTitle) {
if (!this.$data.inputTitle?.value) {
this.$data.inputTitle.value = file_name;
}
}
},
removeDocument: function (object: StoredObject): void {
console.log("catch remove document", object);

View File

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

View File

@@ -23,6 +23,7 @@ const emit =
{
stored_object_version: StoredObjectVersionCreated,
stored_object: StoredObject,
file_name: string,
},
) => void
>();
@@ -114,7 +115,21 @@ const handleFile = async (file: File): Promise<void> => {
persisted: false,
};
emit("addDocument", { stored_object, stored_object_version });
const fileName = file.name;
let file_name = "Nouveau document";
const file_name_split = fileName.split(".");
if (file_name_split.length > 1) {
const extension = file_name_split
? file_name_split[file_name_split.length - 1]
: "";
file_name = fileName.replace(extension, "").slice(0, -1);
}
emit("addDocument", {
stored_object,
stored_object_version,
file_name: file_name,
});
uploading.value = false;
};
</script>

View File

@@ -20,6 +20,7 @@ const emit = defineEmits<{
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
file_name: string,
},
): void;
(e: "removeDocument"): void;
@@ -42,14 +43,16 @@ const buttonState = computed<"add" | "replace">(() => {
function onAddDocument({
stored_object,
stored_object_version,
file_name,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
}): void {
const message =
buttonState.value === "add" ? "Document ajouté" : "Document remplacé";
$toast.success(message);
emit("addDocument", { stored_object_version, stored_object });
emit("addDocument", { stored_object_version, stored_object, file_name });
state.showModal = false;
}

View File

@@ -19,6 +19,7 @@ const emit = defineEmits<{
{
stored_object: StoredObject,
stored_object_version: StoredObjectVersion,
file_name: string,
},
): void;
(e: "removeDocument"): void;
@@ -53,11 +54,13 @@ const dav_link_href = computed<string | undefined>(() => {
const onAddDocument = ({
stored_object,
stored_object_version,
file_name,
}: {
stored_object: StoredObject;
stored_object_version: StoredObjectVersion;
file_name: string;
}): void => {
emit("addDocument", { stored_object, stored_object_version });
emit("addDocument", { stored_object, stored_object_version, file_name });
};
const onRemoveDocument = (e: Event): void => {

View File

@@ -53,7 +53,7 @@ const onRestored = ({
<template>
<template v-if="props.versions.length > 0">
<div class="container">
<template v-for="v in props.versions">
<template v-for="v in props.versions" :key="v.id">
<history-button-list-item
:version="v"
:can-edit="canEdit"

View File

@@ -32,13 +32,17 @@ const onRestore = ({
emit("restoreVersion", { newVersion });
};
const isKeptBeforeConversion = computed<boolean>(() =>
props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
),
);
const isKeptBeforeConversion = computed<boolean>(() => {
if ("point-in-times" in props.version) {
return props.version["point-in-times"].reduce(
(accumulator: boolean, pit: StoredObjectPointInTime) =>
accumulator || "keep-before-conversion" === pit.reason,
false,
);
} else {
return false;
}
});
const isRestored = computed<boolean>(
() => props.version.version > 0 && null !== props.version["from-restored"],
@@ -90,11 +94,11 @@ const classes = computed<{
<div class="col-12">
<file-icon :type="version.type"></file-icon>
<span
><strong>#{{ version.version + 1 }}</strong></span
><strong>&nbsp;#{{ version.version + 1 }}&nbsp;</strong></span
>
<template
v-if="version.createdBy !== null && version.createdAt !== null"
><strong v-if="version.version == 0">Créé par</strong
><strong v-if="version.version == 0">créé par</strong
><strong v-else>modifié par</strong>
<span class="badge-user"
><UserRenderBoxBadge

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") }}
{% endblock %} {% block content %}
<div class="col-md-10 col-xxl">
<div class="document-list">
<h1>
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
</h1>

View File

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

View File

@@ -62,7 +62,15 @@ final readonly class RemoveOldVersionMessageHandler implements MessageHandlerInt
$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
$this->entityManager->remove($storedObjectVersion);

View File

@@ -44,6 +44,7 @@ class RemoveOldVersionMessageHandlerTest extends TestCase
$entityManager->expects($this->once())->method('clear');
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
$storedObjectManager->expects($this->once())->method('exists')->willReturn(true);
$storedObjectManager->expects($this->once())->method('delete')->with($this->identicalTo($version));
$handler = new RemoveOldVersionMessageHandler($storedObjectVersionRepository, new NullLogger(), $entityManager, $storedObjectManager, new MockClock());
@@ -51,6 +52,29 @@ class RemoveOldVersionMessageHandlerTest extends TestCase
$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
{
$object = new StoredObject();
@@ -123,6 +147,6 @@ class DummyStoredObjectManager implements StoredObjectManagerInterface
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:
Document (n°%doc%): "Document (n°%doc%)"
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

@@ -33,6 +33,8 @@
@import './scss/hover.scss';
@import './scss/comment-editor.scss';
/*
* BASE LAYOUT POSITION
*/

View File

@@ -0,0 +1,39 @@
.comment-container {
margin-top: 1.5rem;
}
.toggle-button {
background-color: white;
font-size: .8rem;
text-decoration: none;
position: absolute;
bottom: -10px;
left: 20px;
padding: 2px 6px;
cursor: pointer;
z-index: 10;
transition: left 0.1s ease-in-out;
}
.rich-editor-active .toggle-button {
bottom: 0;
}
.editor-wrapper textarea {
resize: vertical;
min-height: 100px;
}
.editor-container {
position: relative;
display: flex;
flex-direction: column;
}
.editor-wrapper {
position: relative;
}
.hidden-textarea {
display: none;
}

View File

@@ -25,7 +25,34 @@ div.flex-table {
div.item-col:last-child {
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 {

View File

@@ -8,10 +8,10 @@ import {
Heading,
Link,
List,
} from "ckeditor5";
import coreTranslations from "ckeditor5/translations/fr.js";
} from 'ckeditor5';
import coreTranslations from 'ckeditor5/translations/fr.js';
import "ckeditor5/ckeditor5.css";
import 'ckeditor5/ckeditor5.css';
import "./index.scss";
@@ -41,6 +41,8 @@ export default {
"redo",
],
},
translations: [coreTranslations],
translations: [
coreTranslations
],
licenseKey: "GPL",
};
} ;

View File

@@ -1,12 +1,23 @@
import config from "./editor_config";
import { ClassicEditor } from "ckeditor5";
import App from "../../vuejs/CommentEditor/App.vue"
import { createApp, reactive } from "vue";
const ckeditorFields: NodeListOf<HTMLTextAreaElement> =
document.querySelectorAll("textarea[ckeditor]");
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
ClassicEditor.create(field, config).catch((error) => {
console.error(error.stack);
throw error;
});
document.querySelectorAll("[id^='comment-app']");
const globalState = reactive({
isSimple: localStorage.getItem('editorMode') === 'simple'
});
window.addEventListener('storage', () => {
globalState.isSimple = localStorage.getItem('editorMode') === 'simple';
});
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
const app = createApp(App,{
fieldName: field.dataset.fieldName,
template: `<app></app>`
});
app.provide('globalState', globalState)
.component("app", App)
.mount(field);
});
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));

View File

@@ -10,6 +10,10 @@ let appsPerInput = new Map();
function loadDynamicPicker(element) {
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
let suggested;
let as_id;
let submit_on_adding_new_entity;
let label;
apps.forEach(function (el) {
const isMultiple = parseInt(el.dataset.multiple) === 1,

View File

@@ -1,45 +0,0 @@
import { createApp } from "vue";
import OpenWopiLink from "ChillMainAssets/vuejs/_components/OpenWopiLink";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
const i18n = _createI18n({});
//TODO move to chillDocStore or ChillWopi
/*
tags to load module:
<span data-module="wopi-link"
data-wopi-url="{{ path('chill_wopi_file_edit', {'fileId': document.uuid}) }}"
data-doc-type="{{ document.type|e('html_attr') }}"
data-options="{{ options|json_encode }}"
></span>
*/
window.addEventListener("DOMContentLoaded", function (e) {
document
.querySelectorAll('span[data-module="wopi-link"]')
.forEach(function (el) {
createApp({
template:
'<open-wopi-link :wopiUrl="wopiUrl" :type="type" :options="options"></open-wopi-link>',
components: {
OpenWopiLink,
},
data() {
return {
wopiUrl: el.dataset.wopiUrl,
type: el.dataset.docType,
options:
el.dataset.options !== "null"
? JSON.parse(el.dataset.options)
: {},
};
},
})
.use(i18n)
.mount(el);
});
});

View File

@@ -0,0 +1,36 @@
<template>
<div>
<div>
<comment-editor
:isSimple="globalState.isSimple"
:fieldName="fieldName"
@toggle="toggleEditorMode"
></comment-editor>
</div>
</div>
</template>
<script>
import { defineComponent, inject } from 'vue';
import CommentEditor from "../CommentEditor/component/CommentEditor.vue";
export default defineComponent({
name: "App",
components: { CommentEditor },
props: {
fieldName: String
},
setup() {
const globalState = inject('globalState');
const toggleEditorMode = () => {
globalState.isSimple = !globalState.isSimple;
localStorage.setItem('editorMode', globalState.isSimple ? 'simple' : 'rich');
};
return {
globalState,
toggleEditorMode,
};
}
});
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div :class="{'editor-container': true, 'rich-editor-active': !isSimple}">
<div v-if="!isSimple" class="editor-wrapper">
<ckeditor
:name="fieldName"
:editor="classicEditor"
:config="editorConfig"
v-model.lazy="content"
tag-name="textarea"
/>
</div>
<div v-else class="editor-wrapper">
<textarea
v-model.lazy="content"
:name="fieldName"
class="form-control"
></textarea>
</div>
<a @click="toggleSimpleEditor" class="toggle-button">{{ isSimple ? "rich" : "simple" }}</a>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, toRefs } from 'vue';
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
export default defineComponent({
name: "CommentEditor",
components: {
ckeditor: Ckeditor,
},
props: {
type: String,
isSimple: Boolean,
fieldName: String,
},
setup(props, { emit }) {
const { isSimple } = toRefs(props);
const content = ref("");
const classicEditor = ClassicEditor;
const editorConfig = classicEditorConfig;
const toggleSimpleEditor = () => {
emit("toggle");
};
return {
isSimple,
content,
classicEditor,
editorConfig,
toggleSimpleEditor,
};
}
});
</script>

View File

@@ -0,0 +1,14 @@
import {personMessages} from "ChillPersonAssets/vuejs/_js/i18n";
import {calendarUserSelectorMessages} from "ChillCalendarAssets/vuejs/_components/CalendarUserSelector/js/i18n";
import {activityMessages} from "ChillActivityAssets/vuejs/Activity/i18n";
const appMessages = {
fr: {
mode: {
simple: "Editeur simple",
rich: "Editeur riche"
}
},
};
export { appMessages };

View File

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

View File

@@ -1,214 +0,0 @@
<template>
<a
v-if="isOpenDocument"
class="btn"
:class="[
isChangeIcon ? 'change-icon' : '',
isChangeClass ? options.changeClass : 'btn-wopilink',
]"
@click="openModal"
>
<i v-if="isChangeIcon" class="fa me-2" :class="options.changeIcon"></i>
<span v-if="!noText">
{{ trans(WOPI_ONLINE_EDIT_DOCUMENT) }}
</span>
</a>
<teleport to="body">
<div class="wopi-frame" v-if="isOpenDocument">
<modal
v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
:hideFooter="true"
@close="modal.showModal = false"
>
<template #header>
<img class="logo" :src="logo" height="45" />
<span class="ms-auto me-3">
<span v-if="options.title">{{ options.title }}</span>
</span>
</template>
<template #body>
<div v-if="loading" class="loading">
<i
class="fa fa-circle-o-notch fa-spin fa-3x"
:title="trans(WOPI_LOADING)"
></i>
</div>
<iframe :src="this.wopiUrl" @load="loaded"></iframe>
</template>
</modal>
</div>
<div v-else>
<Modal
v-if="modal.showModal"
modalDialogClass="modal-sm"
@close="modal.showModal = false"
>
<template v-slot:header>
<h3>{{ trans(WOPI_INVALID_TITLE) }}</h3>
</template>
<template v-slot:body>
<div class="alert alert-warning">
{{ trans(WOPI_ONLINE_EDIT_DOCUMENT) }}
</div>
</template>
</Modal>
</div>
</teleport>
</template>
<script setup>
import { ref, computed } from "vue";
import {
trans,
WOPI_ONLINE_EDIT_DOCUMENT,
WOPI_INVALID_TITLE,
WOPI_LOADING,
} from "translator";
import Modal from "ChillMainAssets/vuejs/_components/Modal";
import logo from "ChillMainAssets/chill/img/logo-chill-sans-slogan_white.png";
// Props
const props = defineProps({
wopiUrl: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
options: {
type: Object,
required: false,
},
});
// data
const modal = ref({
showModal: false,
modalDialogClass: "modal-fullscreen",
});
const loading = ref(false);
// MIME types
const mime = [
// TODO temporary hardcoded. to be replaced by twig extension or a collabora server query
"application/clarisworks",
"application/coreldraw",
"application/macwriteii",
"application/msword",
"application/pdf",
"application/vnd.lotus-1-2-3",
"application/vnd.ms-excel",
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"application/vnd.ms-excel.sheet.macroEnabled.12",
"application/vnd.ms-excel.template.macroEnabled.12",
"application/vnd.ms-powerpoint",
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
"application/vnd.ms-powerpoint.template.macroEnabled.12",
"application/vnd.ms-visio.drawing",
"application/vnd.ms-word.document.macroEnabled.12",
"application/vnd.ms-word.template.macroEnabled.12",
"application/vnd.ms-works",
"application/vnd.oasis.opendocument.chart",
"application/vnd.oasis.opendocument.formula",
"application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-flat-xml",
"application/vnd.oasis.opendocument.graphics-template",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-flat-xml",
"application/vnd.oasis.opendocument.presentation-template",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-flat-xml",
"application/vnd.oasis.opendocument.spreadsheet-template",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-flat-xml",
"application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-master-template",
"application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-web",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.sun.xml.calc",
"application/vnd.sun.xml.calc.template",
"application/vnd.sun.xml.chart",
"application/vnd.sun.xml.draw",
"application/vnd.sun.xml.draw.template",
"application/vnd.sun.xml.impress",
"application/vnd.sun.xml.impress.template",
"application/vnd.sun.xml.math",
"application/vnd.sun.xml.writer",
"application/vnd.sun.xml.writer.global",
"application/vnd.sun.xml.writer.template",
"application/vnd.visio",
"application/vnd.visio2013",
"application/vnd.wordperfect",
"application/x-abiword",
"application/x-aportisdoc",
"application/x-dbase",
"application/x-dif-document",
"application/x-fictionbook+xml",
"application/x-gnumeric",
"application/x-hwp",
"application/x-iwork-keynote-sffkey",
"application/x-iwork-numbers-sffnumbers",
"application/x-iwork-pages-sffpages",
"application/x-mspublisher",
"application/x-mswrite",
"application/x-pagemaker",
"application/x-sony-bbeb",
"application/x-t602",
];
// Computed
const isOpenDocument = computed(() => mime.includes(props.type));
const noText = computed(() => props.options?.noText === true);
const isChangeIcon = computed(() => !!props.options?.changeIcon);
const isChangeClass = computed(() => !!props.options?.changeClass);
// Methods
const openModal = () => {
loading.value = true;
modal.value.showModal = true;
};
const loaded = () => {
loading.value = false;
};
</script>
<style lang="scss">
div.wopi-frame {
div.modal-header {
border-bottom: 0;
background-color: var(--bs-primary);
color: white;
}
div.modal-body {
padding: 0;
overflow-y: unset !important;
iframe {
height: 100%;
width: 100%;
}
div.loading {
position: absolute;
color: var(--bs-chill-gray);
top: calc(50% - 30px);
left: calc(50% - 30px);
}
}
}
</style>

View File

@@ -54,6 +54,11 @@ const messages = {
residential_address: "Adresse de résidence",
located_at: "réside chez",
},
comment: {
label: "Commentaire",
editor_simple: "Simple",
editor_rich: "Riche"
}
},
};

View File

@@ -136,6 +136,59 @@
</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>
<p>Une liste inline qui s'aligne, puis glisse sous son titre.</p>
<div class="wrap-list debug">
@@ -392,4 +445,12 @@ Toutes les classes btn-* de bootstrap sont fonctionnelles
</div>
</div>
<div class="row">
<h1>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>
{% endblock %}

View File

@@ -214,7 +214,9 @@
{% block private_comment_widget %}
{% for entry in form %}
{{ form_widget(entry) }}
<div id="comment-app-{{ form.vars.id }}" data-field-name="{{ form.vars.full_name }}">
{{ form_widget(entry, { attr: { ckeditor: 'true' } }) }}
</div>
{% endfor %}
{% endblock %}
@@ -224,7 +226,9 @@
{% block comment_widget %}
{% for entry in form %}
{{ form_widget(entry) }}
<div id="comment-app-{{ form.vars.id }}" data-field-name="{{ form.vars.full_name }}">
{{ form_widget(entry, { attr: { ckeditor: 'true' } }) }}
</div>
{% endfor %}
{% endblock comment_widget %}

View File

@@ -9,7 +9,6 @@
{{ encore_entry_script_tags('mod_pickentity_type') }}
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
{{ encore_entry_script_tags('page_workflow_show') }}
{{ encore_entry_script_tags('mod_wopi_link') }}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{{ encore_entry_script_tags('mod_workflow_attachment') }}
{% endblock %}
@@ -19,7 +18,6 @@
{{ encore_entry_link_tags('mod_pickentity_type') }}
{{ encore_entry_link_tags('mod_entity_workflow_subscribe') }}
{{ encore_entry_link_tags('page_workflow_show') }}
{{ encore_entry_link_tags('mod_wopi_link') }}
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{{ encore_entry_link_tags('mod_workflow_attachment') }}
{% endblock %}

View File

@@ -25,6 +25,8 @@ use Symfony\Component\Workflow\Transition;
#[AsMessageHandler]
final readonly class CancelStaleWorkflowHandler
{
private const LOG_PREFIX = '[CancelStaleWorkflowHandler] ';
public function __construct(
private EntityWorkflowRepository $workflowRepository,
private Registry $registry,
@@ -40,13 +42,13 @@ final readonly class CancelStaleWorkflowHandler
$workflow = $this->workflowRepository->find($message->getWorkflowId());
if (null === $workflow) {
$this->logger->alert('Workflow was not found!', [$workflowId]);
$this->logger->alert(self::LOG_PREFIX.'Workflow was not found!', ['entityWorkflowId' => $workflowId]);
return;
}
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');
}
@@ -67,14 +69,14 @@ final readonly class CancelStaleWorkflowHandler
'transitionAt' => $this->clock->now(),
'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;
break;
}
}
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));
}

View File

@@ -86,10 +86,6 @@ module.exports = function (encore, entries) {
"mod_entity_workflow_pick",
__dirname + "/Resources/public/module/entity-workflow-pick/index.js",
);
encore.addEntry(
"mod_wopi_link",
__dirname + "/Resources/public/module/wopi-link/index.js",
);
encore.addEntry(
"mod_pick_postal_code",
__dirname + "/Resources/public/module/pick-postal-code/index.js",

View File

@@ -59,6 +59,7 @@ user_group:
inactive: Inactif
with_users: Membres
no_users: Aucun utilisateur associé
no_user_groups: Aucune groupe d'utilisateurs
no_admin_users: Aucun administrateur
Label: Nom du groupe
BackgroundColor: Couleur de fond du badge
@@ -112,6 +113,8 @@ Any comment: Aucun commentaire
# comment embeddable
No comment associated: Aucun commentaire
private comment: Notes privées
comment_public: Note
comment_private: Note privée
#pagination
Previous: Précédent
@@ -739,6 +742,7 @@ export:
id: Identifiant de l'action
social_issue_id: Identifiant de la problématique sociale
social_issue: Problématique sociale
desactivation_date: Date de désactivation
social_issue_ordering: Ordre de la problématique sociale
action_label: Action d'accompagnement
action_ordering: Ordre

View File

@@ -26,7 +26,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
{
$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;
}

View File

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

View File

@@ -511,6 +511,14 @@ class AccompanyingPeriod implements
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.
*/

View File

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

View File

@@ -22,7 +22,7 @@ use Doctrine\ORM\Mapping as ORM;
class MaritalStatus
{
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 7)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 15)]
private ?string $id;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]

View File

@@ -71,7 +71,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
->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',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
@@ -80,12 +80,15 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
}
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',
'routeParameters' => [
'id' => $period->getId(),
], ])
->setExtras(['order' => 40]);
->setExtras([
'order' => 40,
'counter' => count($period->getWorks()) > 0 ? count($period->getWorks()) : null,
]);
}
$workflow = $this->registry->get($period, 'accompanying_period_lifecycle');

View File

@@ -304,5 +304,14 @@ div#dashboards {
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
div.accompanying-course-work {

View File

@@ -61,15 +61,15 @@
</template>
<script>
import { ClassicEditor } from "ckeditor5";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import {ClassicEditor} from "ckeditor5";
import {Ckeditor} from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { mapState } from "vuex";
export default {
name: "Comment",
components: {
ckeditor: Ckeditor,
ckeditor: Ckeditor
},
data() {
return {

View File

@@ -204,7 +204,8 @@ export default {
} else if (payload.type === "thirdparty") {
body.name = payload.data.text;
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 };
makeFetch(

View File

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

View File

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

View File

@@ -39,15 +39,15 @@
<script>
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { Ckeditor }from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import {ClassicEditor} from "ckeditor5";
export default {
name: "WriteComment",
components: {
Modal,
ckeditor: Ckeditor,
ckeditor: Ckeditor
},
props: ["resource"],
emits: ["updateComment"],

View File

@@ -46,8 +46,7 @@
<label class="col-form-label">{{ $t("comments") }}</label>
<ckeditor
v-model="note"
:editor="classicEditor"
:config="editorConfig"
:editor="classicEditor" :config="editorConfig"
tag-name="textarea"
></ckeditor>
</div>
@@ -208,6 +207,29 @@
</label>
</div>
</li>
<li
v-for="p in getPreviousPersons"
:key="p.id"
class="alert alert-danger"
>
<div class="form-check">
<input
v-model="personsPicked"
:value="p.id"
type="checkbox"
class="me-2 form-check-input"
:id="'person_check' + p.id"
/>
<label :for="'person_check' + p.id" class="form-check-label">
<person-text :person="p"></person-text>
</label>
</div>
<span
><i class="fa fa-warning"></i>&nbsp;{{
$t("warning_previous_persons")
}}</span
>
</li>
</ul>
</div>
@@ -440,7 +462,7 @@
import { mapState, mapGetters } from "vuex";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import {ClassicEditor} from "ckeditor5";
import AddResult from "./components/AddResult.vue";
import AddEvaluation from "./components/AddEvaluation.vue";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
@@ -497,6 +519,8 @@ const i18n = {
notification_notify_referrer: "Notifier le référent",
notification_notify_any: "Notifier d'autres utilisateurs",
notification_send: "Envoyer une notification",
warning_previous_persons:
"Cet usager n'est désormais plus concerné par le parcours, bien qu'il ait été associé à l'action par le passé.",
},
},
};
@@ -583,6 +607,7 @@ export default {
"hasHandlingThirdParty",
"hasThirdParties",
"hasReferrers",
"getPreviousPersons",
]),
classicEditor: () => ClassicEditor,
editorConfig: () => classicEditorConfig,
@@ -745,7 +770,8 @@ export default {
let body = { type: payload.type };
body.name = payload.data.text;
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 };
makeFetch(
@@ -755,7 +781,9 @@ export default {
)
.then((response) => {
this.$store.dispatch("updateThirdParty", response);
this.$refs.onTheFly.closeModal();
for (let otf of this.$refs.onTheFly) {
otf.closeModal();
}
})
.catch((error) => {
if (error.name === "ValidationException") {

View File

@@ -273,7 +273,7 @@
<script>
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import {Ckeditor} from "@ckeditor/ckeditor5-vue";
import { ClassicEditor } from "ckeditor5";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { mapState } from "vuex";
@@ -535,11 +535,11 @@ export default {
title: title,
});
},
addDocument({ stored_object, stored_object_version }) {
addDocument({ stored_object, stored_object_version, file_name }) {
let document = {
type: "accompanying_period_work_evaluation_document",
storedObject: stored_object,
title: "Nouveau document",
title: file_name,
};
this.$store.commit("addDocument", {
key: this.evaluation.key,

View File

@@ -87,6 +87,11 @@ const store = createStore({
return [];
},
getPreviousPersons(state) {
return state.personsPicked.filter(
(p) => !state.personsReachables.map((pr) => pr.id).includes(p.id),
);
},
buildPayload(state) {
return {
type: "accompanying_period_work",
@@ -607,8 +612,7 @@ const store = createStore({
submit({ getters, state, commit }, callback) {
let payload = getters.buildPayload,
params = new URLSearchParams({ entity_version: state.version }),
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json?${params}`,
errors = [];
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json?${params}`;
commit("setIsPosting", true);
// console.log('the social action', payload);

View File

@@ -30,8 +30,7 @@
<div class="item-row comment">
<ckeditor
:editor="classicEditor"
:config="editorConfig"
:editor="classicEditor" :config="editorConfig"
v-model="comment"
tag-name="textarea"
/>
@@ -103,9 +102,9 @@ div.participation-details {
<script>
import { mapGetters } from "vuex";
import PersonRenderBox from "ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import {Ckeditor} from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import {ClassicEditor} from "ckeditor5";
export default {
name: "MemberDetails",

View File

@@ -1,25 +1,25 @@
<template>
<ckeditor
name="content"
:placeholder="
<ckeditor
name="content"
:placeholder="
$t('household_members_editor.positioning.comment_placeholder')
"
:editor="editor"
:config="editorConfig"
v-model="content"
tag-name="textarea"
/>
:editor="editor"
:config="editorConfig"
v-model="content"
tag-name="textarea"
/>
</template>
<script>
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import { ClassicEditor } from "ckeditor5";
import {ClassicEditor} from "ckeditor5";
export default {
name: "PersonComment.vue",
components: {
ckeditor: Ckeditor,
ckeditor: Ckeditor
},
props: ["conc"],
computed: {

View File

@@ -201,7 +201,7 @@
{% endif %}
{% 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">
<h4 class="item-key">{{ 'notification.Notifications'|trans }}</h4>
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
@@ -238,6 +238,31 @@
</div>
</div>
{% 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 class="social-actions my-4">

View File

@@ -8,7 +8,7 @@ L'usager {{ oldPersonLocation|chill_entity_render_string }} a déménagé.
Son adresse était utilisée pour localiser le parcours n°{{ period.id }}, dont vous êtes
le référent.
En conséquence de ce déménage, le parcours est toujours localisé à cette adresse, mais à l'aide d'une
En conséquence de ce déménagement, le parcours est toujours localisé à cette adresse, mais à l'aide d'une
adresse temporaire.
Si vous continuez à suivre le parcours, vous pouvez le localiser à nouveau auprès de l'adresse de

View File

@@ -5,44 +5,49 @@
{% set w = document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork %}
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ w.accompanyingPeriod.id }}
</span>&nbsp;
<!-- evaluation document -->
<div class="item-two-col-grid" style="width: unset">
<div class="title">
{% if document.storedObject.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.storedObject.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.storedObject.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% 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>
{{ 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>
{% 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>

View File

@@ -30,9 +30,6 @@ final readonly class SocialActionCSVExportService
private TranslatorInterface $translator,
) {}
/**
* @param list<SocialAction> $actions
*/
public function generateCsv(array $actions): Writer
{
// CSV headers
@@ -84,7 +81,8 @@ final readonly class SocialActionCSVExportService
'action_id' => $action->getId(),
'social_issue_id' => $action->getIssue()?->getId(),
'problematique_label' => null !== $action->getIssue() ? $this->socialIssueRender->renderString($action->getIssue(), []) : null,
'social_issue_ordering' => null !== $action->getIssue() ? $action->getIssue()->getOrdering() : null,
'desactivation_date' => $action->getDesactivationDate()?->format('Y-m-d'),
'social_issue_ordering' => $action->getIssue()?->getOrdering(),
'action_label' => $this->socialActionRender->renderString($action, []),
'action_ordering' => $action->getOrdering(),
'goal_label' => null !== $goal ? $this->stringHelper->localize($goal->getTitle()) : null,

View File

@@ -44,6 +44,7 @@ readonly class SocialIssueCSVExportService
'Id',
'Label',
'Social issue',
'export.social_action_list.desactivation_date',
'socialIssue.isParent?',
'socialIssue.Parent id',
]
@@ -66,6 +67,7 @@ readonly class SocialIssueCSVExportService
'id' => $issue->getId(),
'label' => $this->stringHelper->localize($issue->getTitle()),
'title' => $this->socialIssueRender->renderString($issue, []),
'export.social_action_list.desactivation_date' => $issue->getDesactivationDate()?->format('Y-m-d'),
'isParent' => $issue->hasChildren() ? 'X' : '',
'parent_id' => null !== $issue->getParent() ? $issue->getParent()->getId() : '',
];

View File

@@ -52,6 +52,7 @@ class SocialActionCsvExporterTest extends TestCase
// Création d'une instance réelle de SocialAction sans objectifs ni résultats
$actionWithoutGoalsOrResults = new SocialAction();
$actionWithoutGoalsOrResults->setIssue($socialIssue);
$actionWithoutGoalsOrResults->setDesactivationDate(new \DateTime('2025-05-21'));
$actionWithoutGoalsOrResults->setTitle(['fr' => 'Action without goals or results']);
// Création d'une instance réelle de SocialAction avec des objectifs et des résultats
@@ -61,6 +62,7 @@ class SocialActionCsvExporterTest extends TestCase
$actionWithGoalsAndResults = new SocialAction();
$actionWithGoalsAndResults->setIssue($socialIssue);
$actionWithGoalsAndResults->setDesactivationDate(new \DateTime('2025-05-21'));
$actionWithGoalsAndResults->setTitle(['fr' => 'Action with goals and results']);
$actionWithGoalsAndResults->addGoal($goalWithResult);
@@ -68,6 +70,7 @@ class SocialActionCsvExporterTest extends TestCase
$goalWithoutResult = new Goal();
$actionWithGoalsNoResults = new SocialAction();
$actionWithGoalsNoResults->setIssue($socialIssue);
$actionWithGoalsNoResults->setDesactivationDate(new \DateTime('2025-05-21'));
$actionWithGoalsNoResults->setTitle(['fr' => 'Action with goals and no results']);
$actionWithGoalsNoResults->addGoal($goalWithoutResult);
@@ -76,6 +79,7 @@ class SocialActionCsvExporterTest extends TestCase
$resultWithNoAction->setTitle(['fr' => 'Result without objectives']);
$actionWithResultsNoGoals = new SocialAction();
$actionWithResultsNoGoals->setIssue($socialIssue);
$actionWithResultsNoGoals->setDesactivationDate(new \DateTime('2025-05-21'));
$actionWithResultsNoGoals->setTitle(['fr' => 'Action with results and no goals']);
$actionWithResultsNoGoals->addResult($resultWithNoAction);
@@ -91,11 +95,11 @@ class SocialActionCsvExporterTest extends TestCase
$this->assertStringContainsString('Action with results and no goals', $content);
self::assertEquals(<<<'CSV'
export.social_action_list.action_id,export.social_action_list.social_issue_id,export.social_action_list.problematique_label,export.social_action_list.social_issue_ordering,export.social_action_list.action_label,export.social_action_list.action_ordering,export.social_action_list.goal_label,export.social_action_list.goal_id,export.social_action_list.goal_result_label,export.social_action_list.goal_result_id,export.social_action_list.result_without_goal_label,export.social_action_list.result_without_goal_id,export.social_action_list.evaluation_title,export.social_action_list.evaluation_id,export.social_action_list.evaluation_url,export.social_action_list.evaluation_delay_month,export.social_action_list.evaluation_delay_week,export.social_action_list.evaluation_delay_day
,,"Issue Title",0,"Action with goals and results",0,"not found",,"not found",,,,,,,,,
,,"Issue Title",0,"Action without goals or results",0,,,,,,,,,,,,
,,"Issue Title",0,"Action with goals and no results",0,"not found",,,,,,,,,,,
,,"Issue Title",0,"Action with results and no goals",0,,,,,"Result without objectives",,,,,,,
export.social_action_list.action_id,export.social_action_list.social_issue_id,export.social_action_list.problematique_label,export.social_action_list.desactivation_date,export.social_action_list.social_issue_ordering,export.social_action_list.action_label,export.social_action_list.action_ordering,export.social_action_list.goal_label,export.social_action_list.goal_id,export.social_action_list.goal_result_label,export.social_action_list.goal_result_id,export.social_action_list.result_without_goal_label,export.social_action_list.result_without_goal_id,export.social_action_list.evaluation_title,export.social_action_list.evaluation_id,export.social_action_list.evaluation_url,export.social_action_list.evaluation_delay_month,export.social_action_list.evaluation_delay_week,export.social_action_list.evaluation_delay_day
,,"Issue Title",2025-05-21,0,"Action with goals and results",0,"not found",,"not found",,,,,,,,,
,,"Issue Title",2025-05-21,0,"Action without goals or results",0,,,,,,,,,,,,
,,"Issue Title",2025-05-21,0,"Action with goals and no results",0,"not found",,,,,,,,,,,
,,"Issue Title",2025-05-21,0,"Action with results and no goals",0,,,,,"Result without objectives",,,,,,,
CSV, $content);
}

View File

@@ -0,0 +1,43 @@
<?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\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250514115009 extends AbstractMigration
{
public function getDescription(): string
{
return 'Allow more characters for maritalstatus id';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_person_marital_status ALTER id TYPE VARCHAR(15)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_person_person ALTER maritalstatus_id TYPE VARCHAR(15)
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_person_person ALTER maritalStatus_id TYPE VARCHAR(7)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_person_marital_status ALTER id TYPE VARCHAR(7)
SQL);
}
}

View File

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

View File

@@ -624,7 +624,7 @@ final class SingleTaskController extends AbstractController
->addCheckbox('status', $statuses, $statuses, $statusTrans);
$states = $this->singleTaskStateRepository->findAllExistingStates();
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['closed', 'canceled', 'validated'], true)));
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['in_progress', 'closed', 'canceled', 'validated'], true)));
if ([] !== $states) {
$filterBuilder

View File

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

View File

@@ -209,6 +209,11 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
#[PhonenumberConstraint(type: 'any')]
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)]
private ?array $thirdPartyTypes = [];
@@ -429,6 +434,11 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
return $this->telephone;
}
public function getTelephone2(): ?PhoneNumber
{
return $this->telephone2;
}
/**
* Get type.
*/
@@ -712,6 +722,13 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
return $this;
}
public function setTelephone2(?PhoneNumber $telephone2 = null): self
{
$this->telephone2 = $telephone2;
return $this;
}
/**
* Set type.
*

View File

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

View File

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

View File

@@ -91,6 +91,18 @@
}}</a
>
</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">
<i
class="fa fa-li fa-envelope-o"
@@ -121,6 +133,12 @@
thirdparty.telephone
}}</a>
</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">
<i class="fa fa-li fa-envelope-o" />
<a :href="'mailto: ' + thirdparty.email">{{

View File

@@ -223,6 +223,19 @@
/>
</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 class="input-group mb-3">
<span class="input-group-text" id="comment"
@@ -263,6 +276,7 @@ export default {
firstname: "",
name: "",
telephone: "",
telephone2: "",
civility: null,
profession: "",
},
@@ -368,9 +382,11 @@ export default {
addQueryItem(field, queryItem) {
switch (field) {
case "name":
this.thirdparty.name
? (this.thirdparty.name += ` ${queryItem}`)
: (this.thirdparty.name = queryItem);
if (this.thirdparty.name) {
this.thirdparty.name += ` ${queryItem}`;
} else {
this.thirdparty.name = queryItem;
}
break;
case "firstName":
this.thirdparty.firstname = queryItem;

View File

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

View File

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

View File

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

View File

@@ -24,17 +24,24 @@
</div>
</div>
<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_errors(form.telephone) }}
{{ form_label(form.telephone) }}
</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_errors(form.email) }}
{{ form_label(form.email) }}
</div>
<div class="form-group col-md-2 mb-3">
<div class="form-group col-md-6 mb-3">
{{ form_widget(form.contactDataAnonymous) }}
{{ form_label(form.contactDataAnonymous) }}
{{ form_errors(form.contactDataAnonymous) }}

View File

@@ -76,6 +76,18 @@
{% endif %}
</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>
<dd>
{% if thirdParty.email == null %}

View File

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

View File

@@ -28,6 +28,8 @@ components:
type: string
telephone:
type: string
telephone2:
type: string
address:
$ref: "#/components/schemas/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
name: Nom
telephone: Téléphone
telephone2: Autre numéro de téléphone
adress: Adresse
email: Courriel
comment: Commentaire
@@ -39,7 +40,7 @@ thirdparty.A contact: Une personne physique
thirdparty.contact: Personne physique
thirdparty.Contact of: Contact de
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.
thirdparty.a_contact_explanation: >-
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 firstname: Prénom 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 address: Adresse du contact
Contact profession: Profession du contact