Compare commits

..

91 Commits

Author SHA1 Message Date
e00ece4200 Update form builder parameter in SearchController
Changed the first argument in the `createNamedBuilder` method from `null` to an empty string. This adjustment ensures the form factory correctly creates the builder in the SearchController.
2024-05-28 15:58:17 +02:00
640fd71402 merge ticket-app-master and fix rector / cs 2024-05-28 15:54:52 +02:00
aae50ca290 Merge branch 'ticket-app-master' into chill-bundles-ticket-app-adaptations 2024-05-28 15:08:59 +02:00
1fa483598b Merge branch 'upgrade-sf5' into ticket-app-master 2024-05-28 14:59:25 +02:00
e4b6a468f8 adding fixtures for ticket in every environment 2024-05-28 13:47:58 +02:00
Boris Waaub
66c7758023 Adapt module name 2024-05-22 11:17:07 +02:00
Boris Waaub
4750d2c24e Adapt module name 2024-05-22 11:16:18 +02:00
Boris Waaub
ca05e3d979 Layout adaptation 2024-05-22 11:12:22 +02:00
Boris Waaub
a20f9b4f86 Generalize ticket actions 2024-05-22 00:38:47 +02:00
Boris Waaub
c73c1eb8d5 Rename "appelant" by "patient" 2024-05-21 22:24:30 +02:00
Boris Waaub
8778bb0731 Use colors and badges for history and banner 2024-05-21 22:22:33 +02:00
Boris Waaub
c7d20eebc5 chore: Remove unused code in AddresseeSelectorComponent.vue 2024-05-21 20:53:15 +02:00
Boris Waaub
b9e130c159 Use suggestion for user asignee 2024-05-21 20:44:23 +02:00
Boris Waaub
3e8bc94af3 Remove user object display 2024-05-21 18:14:11 +02:00
Boris Waaub
0c914c9f9f Remove "remove_addressee" history line 2024-05-21 17:32:40 +02:00
Boris Waaub
580a60c939 Add user_group for returning type 2024-05-21 17:32:05 +02:00
Boris Waaub
4996ac3b7c Adapt layout action toolbar 2024-05-21 15:22:13 +02:00
Boris Waaub
2a23bf19cb use record_actions sticky-form-buttons 2024-05-21 10:53:25 +02:00
Boris Waaub
650d2596d9 Update ticket display to use ticket ID instead of external reference 2024-05-21 09:54:06 +02:00
Boris Waaub
2bdd5a329e Merge branch 'ticket-app-master' of gitlab.com:boriswa/chill-bundles into ticket-app-master 2024-05-21 09:53:32 +02:00
78d1776733 Add functionality to find a caller by phone number
Added a new method in PersonRepository to allow querying people by phone number. Also, a new REST API endpoint "/public/api/1.0/ticket/find-caller" was introduced and it can find a caller by their phone number. Accompanied this feature addition with corresponding test cases.
2024-05-17 13:14:26 +02:00
66dc603c85 fix cs with new version of php-cs-fixer 2024-05-17 12:20:33 +02:00
3a8154ecce Replace PhoneNumberUtil with PhonenumberHelper
The PhoneNumberUtil has been replaced with PhonenumberHelper in AssociateByPhonenumberCommandHandler and its test class. The purpose of this change is to improve phone number parsing which is now delegated to the PhonenumberHelper class in the Chill\MainBundle\Phonenumber namespace. As a consequence, the related dependencies in both the service and the test class have been updated accordingly.
2024-05-17 12:17:00 +02:00
c81828e04f Add phone number parsing functionality
Added a new method 'parse' in the PhonenumberHelper class in ChillMainBundle to sanitize and parse phone numbers. This method specifically handles phone numbers that start with '00', '+' or '0'. Associated unit tests for this new method were also added in PhonenumberHelperTest.php.
2024-05-17 12:16:28 +02:00
Boris Waaub
ec17dd7de2 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles into ticket-app-master 2024-05-13 16:08:19 +02:00
76c076a5f3 Merge branch 'ticket-app-create-template' into 'ticket-app-master'
Mise à jour des messages de l'interface utilisateur pour inclure les...

See merge request Chill-Projet/chill-bundles!689
2024-05-13 13:34:43 +00:00
Boris Waaub
f0045edd6c FIX: Ouvert depuis 2024-05-13 12:33:11 +02:00
Boris Waaub
d00b76ffcd $tc n'est plus supporté pour i18n composition api, il faut utiliser $t.
FIX: Person PersonRenderBox
2024-05-13 12:16:07 +02:00
Boris Waaub
8991f0ef3f Modification i18n 2024-05-13 12:00:11 +02:00
Boris Waaub
d6f5eae0c9 Rendre les commentaire markdown 2024-05-13 11:59:50 +02:00
Boris Waaub
821fce3dd8 $tc n'est plus supporté pour i18n composiontion api, il faut utiliser $t.
Source : https://github.com/intlify/vue-cli-plugin-i18n/issues/214
i18n composion api : https://vue-i18n.intlify.dev/api/composition
2024-05-13 11:38:28 +02:00
Boris Waaub
1d33ae1e39 use ckeditor 2024-05-08 18:03:50 +02:00
Boris Waaub
19af0feb57 Use PersonRenderBox 2024-05-08 17:54:03 +02:00
Boris Waaub
1c09e9a692 Merge branch 'ticket-app-master' into ticket-app-create-template 2024-05-08 16:05:35 +02:00
Boris Waaub
d72e748388 Merge branch 'ticket-app-master' of https://gitlab.com/boriswa/chill-bundles into ticket-app-master 2024-05-08 16:02:09 +02:00
Boris Waaub
ab850b7b70 Fusionner les utilisateurs/goupes en une "Card" 2024-05-06 20:07:15 +02:00
Boris Waaub
3f9745d8cf Use teleport for banner 2024-05-06 18:03:04 +02:00
Boris Waaub
473765366a Add tranfert with AddPerson 2024-05-06 16:38:56 +02:00
Boris Waaub
6500c24a7f Déplacer le répertoire translation dans source 2024-05-02 14:10:22 +02:00
Boris Waaub
1d00457141 Ajouter les propriétés createdAt et updatedBy à l'interface Ticket 2024-05-02 14:09:52 +02:00
Boris Waaub
eb0bf56cff Add user group addressee 2024-05-02 13:18:45 +02:00
Boris Waaub
7b8cd90cf1 Add user store 2024-05-02 12:03:10 +02:00
Boris Waaub
a27d92aba0 Add comment and motive 2024-05-02 00:50:33 +02:00
Boris Waaub
85bdfb9e21 Remove banner component 2024-05-01 22:04:07 +02:00
Boris Waaub
4cffcf4de1 Use translate in setup 2024-05-01 22:03:36 +02:00
Boris Waaub
b2587a688f Déplacer le composant banner dans twig 2024-05-01 15:51:12 +02:00
Boris Waaub
c9f0e9843b Déplacer le composant banner dans twig 2024-05-01 15:49:32 +02:00
Boris Waaub
b40ad9e445 Mise à jour des messages de l'interface utilisateur pour inclure les fonctionnalités de commentaire, de motif et de transfert 2024-04-25 11:16:08 +02:00
Boris Waaub
3e10e47e29 Merge branch 'ticket-app-master' into ticket-app-create-template 2024-04-25 10:37:42 +02:00
Boris Waaub
2a1963e993 Mise à jour de l'interface utilisateur pour le composant ActionToolbarComponent 2024-04-25 10:36:45 +02:00
34c171659b Merge branch 'ticket-app/backend-3' into 'ticket-app-master'
Add functionality to set addressees for a ticket

See merge request Chill-Projet/chill-bundles!683
2024-04-24 16:50:29 +00:00
2d8b960d9e Re-open the same ticket if a ticket already exists with the same externalRef, instead of creating a new one 2024-04-24 18:48:00 +02:00
831ae03431 Merge branch 'ticket-app/backend-2' into 'ticket-app-master'
Add functionality to add comments to tickets

See merge request Chill-Projet/chill-bundles!681
2024-04-23 21:42:07 +00:00
45828174d1 Add addressee history to ticket serialization
This update extends the tickets serialization and normalisation process to include addressee history. With the changes, AddresseeHistory class now also keeps track of who removed an addressee. Additional types, tests and interfaces have been introduced to support this change.
2024-04-23 23:39:01 +02:00
ed45f14a45 Add tracking of addressee history in ticket system
The updates introduce tracking for the history of addressees in the ticket system, both when added and when removed. The user who removed an addressee is now recorded. The changes also ensure these updated aspects are correctly normalized and users can see them in the ticket history. A new database migration file was created for the changes.
2024-04-23 23:38:34 +02:00
fa67835690 Add functionality to add single addressee to tickets
This update introduces a new feature allowing end-users to add a single addressee to a ticket without removing the existing ones. This was achieved by adding a new API endpoint and updating the SetAddresseesController to handle the addition of a single addressee. Accompanying tests have also been provided to ensure the new feature works as expected.
2024-04-23 23:00:12 +02:00
b434d38091 Add functionality to set addressees for a ticket
This update includes the implementation of methods to add and retrieve addressee history in the Ticket entity, a handler for addressee setting command, denormalizer for transforming request data to SetAddresseesCommand, and corresponding tests. Additionally, it adds a SetAddresseesController for handling addressee related requests and updates the API specifications.
2024-04-23 22:50:51 +02:00
Boris Waaub
800a952532 Add base template 2024-04-23 20:41:32 +02:00
9f355032a8 Create a "do not exclude" validation constraint for user groups 2024-04-22 12:41:43 +02:00
0bc6e62d4d Add fixtures for UserGroup 2024-04-22 12:01:49 +02:00
46fb1c04b5 Add color and exclusion fields to UserGroup
This commit introduces new fields to the UserGroup entity, specifically background color, foreground color, and an exclusion key. These have been implemented both in the PHP entity and TypeScript interface definitions. Additionally, a Doctrine migration has been created to reflect these changes on the database side.
2024-04-22 12:01:28 +02:00
3b2c3d1464 Merge branch 'ticket-app-create-store' into 'ticket-app-master'
Create vuex store

See merge request Chill-Projet/chill-bundles!678
2024-04-22 08:29:56 +00:00
Boris Waaub
0bd6038160 Merge branch chill-bundles:master into ticket-app-master 2024-04-19 15:54:24 +00:00
Boris Waaub
baab8e94ce Add ticket to storeand catch error with toast in component 2024-04-19 17:46:12 +02:00
e2deb55fdb Create api endpoint for listing user-group 2024-04-19 15:34:43 +02:00
Boris Waaub
2cdfb50058 Mise en œuvre de la fonctionnalité de remplacement du motif du ticket
La validation introduit plusieurs fonctionnalités liées à la gestion du motif du ticket dans le bundle Chill-TicketBundle :
- Ajoute la possibilité de remplacer le motif d'un ticket par un nouveau.
- Fournit des fonctionnalités de gestion de l'historique des motifs du ticket.
- Implémente les modifications pertinentes au niveau du contrôleur, du gestionnaire d'actions et de l'entité.
- Intègre de nouvelles points d'API et met à jour le fichier de spécification de l'API pour la nouvelle fonctionnalité.
- Inclut des tests pour garantir le bon fonctionnement de la nouvelle fonctionnalité.
2024-04-19 14:12:09 +02:00
39d701feb2 Serialize ticket's Comment 2024-04-18 22:10:56 +02:00
613ee8b186 Add functionality to add comments to tickets
A new controller, 'AddCommentController', has been added. This controller implements the 'AddCommentCommandHandler', allowing users to add comments to tickets. Additionally, corresponding test cases were implemented. The Ticket entity was also updated to accept and manage comments. API endpoint specs were updated to reflect these changes.
2024-04-18 21:57:55 +02:00
56a1a488de Return the content of the ticket on replace motive POST request 2024-04-18 15:44:05 +02:00
3f789ad0f4 Merge branch 'ticket-app/create-entities' into 'ticket-app-master'
Add phone number search function to PersonACLAwareRepository

See merge request Chill-Projet/chill-bundles!677
2024-04-18 11:21:46 +00:00
467bea7cde Serialization of tickets with history 2024-04-18 13:13:09 +02:00
670b8eb82b Implement functionality to replace ticket's motive
The commit introduces several features related to ticket motive management in the Chill-TicketBundle:
- Adds capability to replace a ticket's motive with a new one.
- Provides ticket motive history management features.
- Implements relevant changes in Controller, Action Handler, and Entity levels.
- Incorporates new API endpoints and updates the API specification file for the new feature.
- Includes tests to ensure the new functionality works as expected.
2024-04-18 13:13:08 +02:00
a9760b323f Add ChillTicketBundle to configuration and autoload-dev
The commit includes the ChillTicketBundle in the bundles configuration file for testing. Additionally, the autoload-dev directive in the composer.json file was updated to include the "App" namespace for testing purposes. This ensures that the tests related to the "App" namespace are correctly autoloaded.
2024-04-18 13:13:08 +02:00
71a3a1924a Add Motive API and related fixtures to ChillTicketBundle
This update introduces the Motive API Controller to the ChillTicket bundle with its corresponding service configuration. Also included are related data fixtures for loading motive information. The motive entity has been updated to improve its serialization properties and new types were added to the TypeScript definitions of the bundle.
2024-04-18 13:13:07 +02:00
ecdc1e25bf Layout of banner for ticket 2024-04-18 13:13:07 +02:00
dd37427be1 Bootstrap ticket layout and vue app to edit ticket 2024-04-18 13:13:07 +02:00
c8467df1b1 fixup! Rename Command directory to Action to avoid confusion with symfony commands 2024-04-18 13:13:06 +02:00
4c89a954fa Refactor test, fixing the constructor 2024-04-18 13:13:05 +02:00
7c1f3b114d Rename Command directory to Action to avoid confusion with symfony commands 2024-04-18 13:13:05 +02:00
36bc4dab24 Configure a testsuite for TicketBundle 2024-04-18 13:13:04 +02:00
4b30d92282 Add ticket creation and associating by phone number functionality
This update introduces new features allowing the creation of tickets and associating them with a phone number. Specifically, relevant commands and their handlers have been created along with corresponding tests. An endpoint for ticket creation has also been set up, and the ViewTicketController has been renamed and refactored to EditTicketController to better reflect its function.
2024-04-18 13:13:04 +02:00
75fbec5489 Create entities and doctrine mapping for ticket 2024-04-18 13:13:03 +02:00
912fdd6349 Add phone number search function to PersonACLAwareRepository
A new function, findByPhone, has been added to the PersonACLAwareRepository. This function allows searching for people based on their phone numbers. Changes also reflect in the PersonACLAwareRepositoryInterface, and new test cases have been added to the PersonACLAwareRepositoryTest.
2024-04-16 14:41:55 +02:00
5832542978 load also tests for ticket bundle 2024-04-16 14:41:39 +02:00
5c3585a1ed Fix loading of environment variable in bootstrap process 2024-04-16 14:41:29 +02:00
a2f1e20ddf Fix cs 2024-04-15 15:49:47 +02:00
4d67702a76 Bootstrap loading of controllers and routes for ticket bundle 2024-04-15 15:48:25 +02:00
18e442db29 Merge branch 'ticket-app-init' into 'ticket-app-master'
Add ChillTicketBundle webpack configuration

See merge request Chill-Projet/chill-bundles!673
2024-04-15 12:44:21 +00:00
Boris Waaub
deb3d92189 Add ChillTicketBundle webpack configuration 2024-04-15 14:34:09 +02:00
a59ea7db31 Compiles with ticket bundle 2024-04-15 13:48:49 +02:00
a738b0cac9 Initialize ChillTicketBundle 2024-04-15 13:22:36 +02:00
273 changed files with 7018 additions and 4074 deletions

View File

@@ -1,6 +0,0 @@
kind: Feature
body: |
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
time: 2024-05-30T16:00:03.440767606+02:00
custom:
Issue: ""

View File

@@ -1,6 +0,0 @@
kind: Feature
body: |
Upgrade CKEditor and refactor configuration with use of typescript
time: 2024-05-31T19:02:42.776662753+02:00
custom:
Issue: ""

View File

@@ -1,8 +0,0 @@
kind: Feature
body: |-
Electronic signature
Implementation of the electronic signature for documents within chill.
time: 2024-06-14T15:32:36.875891692+02:00
custom:
Issue: ""

View File

@@ -1,5 +0,0 @@
kind: Feature
body: Metadata form added for person signatures
time: 2024-07-18T15:12:33.8134266+02:00
custom:
Issue: "288"

View File

@@ -1,21 +0,0 @@
## v2.20.0 - 2024-06-05
### Fixed
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
* Added translations for choices of durations (> 5 hours)
### Feature
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
This endpoint should be added to make the endpoint works properly:
```yaml
security:
firewalls:
dav:
pattern: ^/dav
provider: chain_provider
stateless: true
guard:
authenticators:
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
```

View File

@@ -1,3 +0,0 @@
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons

View File

@@ -1,31 +0,0 @@
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
*
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
*

View File

@@ -6,64 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
*
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
*
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons
## v2.20.0 - 2024-06-05
### Fixed
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
* Added translations for choices of durations (> 5 hours)
### Feature
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
This endpoint should be added to make the endpoint works properly:
```yaml
security:
firewalls:
dav:
pattern: ^/dav
provider: chain_provider
stateless: true
guard:
authenticators:
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
```
## v2.19.0 - 2024-05-14 ## v2.19.0 - 2024-05-14
### Feature ### Feature
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side * ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side

View File

@@ -55,7 +55,7 @@ Arborescence:
- person - person
- personvendee - personvendee
- household_edit_metadata - household_edit_metadata
- index.js - index.ts
``` ```
## Organisation des feuilles de styles ## Organisation des feuilles de styles

View File

@@ -19,6 +19,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0", "doctrine/orm": "^2.13.0",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu-bundle": "^3.0", "knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12", "knplabs/knp-time-bundle": "^1.12",
"knpuniversity/oauth2-client-bundle": "^2.10", "knpuniversity/oauth2-client-bundle": "^2.10",
@@ -31,7 +32,6 @@
"phpoffice/phpspreadsheet": "^1.16", "phpoffice/phpspreadsheet": "^1.16",
"ramsey/uuid-doctrine": "^1.7", "ramsey/uuid-doctrine": "^1.7",
"sensio/framework-extra-bundle": "^5.5", "sensio/framework-extra-bundle": "^5.5",
"smalot/pdfparser": "^2.10",
"spomky-labs/base64url": "^2.0", "spomky-labs/base64url": "^2.0",
"symfony/asset": "^5.4", "symfony/asset": "^5.4",
"symfony/browser-kit": "^5.4", "symfony/browser-kit": "^5.4",
@@ -92,12 +92,12 @@
"phpstan/phpstan": "^1.9", "phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-strict-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^10.5.24", "phpunit/phpunit": ">= 7.5",
"rector/rector": "^1.1.0", "rector/rector": "^1.1.0",
"symfony/debug-bundle": "^5.4", "symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4", "symfony/dotenv": "^5.4",
"symfony/maker-bundle": "^1.20", "symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^7.1", "symfony/phpunit-bridge": "^5.4",
"symfony/runtime": "^5.4", "symfony/runtime": "^5.4",
"symfony/stopwatch": "^5.4", "symfony/stopwatch": "^5.4",
"symfony/var-dumper": "^5.4" "symfony/var-dumper": "^5.4"
@@ -119,6 +119,7 @@
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle", "Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle", "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
"Chill\\Utils\\Rector\\": "utils/rector/src" "Chill\\Utils\\Rector\\": "utils/rector/src"
@@ -126,8 +127,9 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"App\\": "tests/", "App\\": "tests",
"Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\TicketBundle\\Tests\\": "src/Bundle/ChillTicketBundle/tests",
"Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests" "Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests"
} }
@@ -149,7 +151,6 @@
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd" "cache:clear": "symfony-cmd"
}, }
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none"
} }
} }

View File

@@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine.
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_ in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
or in the :code:`.env.local` file, which should not be committed to the git repository. or in the :code:`.env.local` file, which should not be commited to the git repository.
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
by the symfony server, from the docker compose services. by the symfony server, from the docker compose services.
@@ -114,12 +114,6 @@ you can either:
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
not the password in clear text). not the password in clear text).
- set up the jwt authentication bundle
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command
:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY`
and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill.
Prepare migrations and other tools Prepare migrations and other tools
********************************** **********************************
@@ -170,7 +164,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t
You will require some bundles to have the following development tools: You will require some bundles to have the following development tools:
- add fixtures - add fixtures
- add profiler and debug bundle - add profiler and var-dumper to debug
Install fixtures Install fixtures
**************** ****************
@@ -185,7 +179,7 @@ Install fixtures
This will generate user accounts, centers, and some basic configuration. This will generate user accounts, centers, and some basic configuration.
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`. visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
The password is always :code:`password`. The password is always :code:`password`.
@@ -198,7 +192,7 @@ Add web profiler and debugger
.. code-block:: bash .. code-block:: bash
symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
Working on chill bundles Working on chill bundles
************************ ************************

View File

@@ -6,13 +6,12 @@
"@apidevtools/swagger-cli": "^4.0.4", "@apidevtools/swagger-cli": "^4.0.4",
"@babel/core": "^7.20.5", "@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@ckeditor/ckeditor5-build-classic": "^41.4.2", "@ckeditor/ckeditor5-build-classic": "^35.3.2",
"@ckeditor/ckeditor5-dev-utils": "^40.2.0", "@ckeditor/ckeditor5-dev-utils": "^31.1.13",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", "@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-dev-translations": "^40.2.0", "@ckeditor/ckeditor5-markdown-gfm": "^35.3.2",
"@ckeditor/ckeditor5-markdown-gfm": "^41.4.2", "@ckeditor/ckeditor5-theme-lark": "^35.3.2",
"@ckeditor/ckeditor5-theme-lark": "^41.4.2", "@ckeditor/ckeditor5-vue": "^4.0.1",
"@ckeditor/ckeditor5-vue": "^5.1.0",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node14": "^1.0.1", "@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
@@ -32,7 +31,7 @@
"select2-bootstrap-theme": "0.1.0-beta.10", "select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"ts-loader": "^9.3.1", "ts-loader": "^9.3.1",
"typescript": "^5.4.5", "typescript": "^4.7.2",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
@@ -46,11 +45,9 @@
"@fullcalendar/vue3": "^6.1.4", "@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/dompurify": "^3.0.5",
"dropzone": "^5.7.6", "dropzone": "^5.7.6",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"marked": "^12.0.2",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"mime": "^4.0.0", "mime": "^4.0.0",
"swagger-ui": "^4.15.5", "swagger-ui": "^4.15.5",

View File

@@ -49,6 +49,10 @@
<!-- temporarily removed, the time to find a fix --> <!-- temporarily removed, the time to find a fix -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
</testsuite> </testsuite>
<testsuite name="TicketBundle">
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
</testsuite>
<!-- <!--
<testsuite name="ReportBundle"> <testsuite name="ReportBundle">
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory> <directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>

View File

@@ -1,49 +0,0 @@
<?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\ActivityBundle\Menu;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new activity in accompanying course', [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
// 'activityType_id' => '',
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 10,
'icon' => 'plus',
])
;
}
}
}

View File

@@ -68,7 +68,7 @@
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div> <div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list"> <div class="wl-col list">
<p class="wl-item"> <p class="wl-item">
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span> <span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -87,8 +87,7 @@
<li> <li>
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
hello {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span> </span>
{% else %} {% else %}
{{ _self.insert_onthefly(bloc.type, item) }} {{ _self.insert_onthefly(bloc.type, item) }}
@@ -115,7 +114,7 @@
<li> <li>
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
</span> </span>
{% else %} {% else %}
{{ _self.insert_onthefly(bloc.type, item) }} {{ _self.insert_onthefly(bloc.type, item) }}
@@ -143,7 +142,7 @@
<span class="wl-item"> <span class="wl-item">
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %} {%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
{% set invite = entity.inviteForUser(item) %} {% set invite = entity.inviteForUser(item) %}
{% if invite is not null %} {% if invite is not null %}

View File

@@ -41,7 +41,7 @@
{% if activity.user and t.userVisible %} {% if activity.user and t.userVisible %}
<li> <li>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span> <span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span> <span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
</li> </li>
{% endif %} {% endif %}

View File

@@ -37,7 +37,7 @@
{%- if entity.user is not null %} {%- if entity.user is not null %}
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt> <dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd> <dd>
<span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span> <span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
</dd> </dd>
{% endif %} {% endif %}

View File

@@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
throw new \RuntimeException('Could not determine context of activity.'); throw new \RuntimeException('Could not determine context of activity.');
} }
} elseif ($subject instanceof AccompanyingPeriod) { } elseif ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) { if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) { if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
return false; return false;
} }

View File

@@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase
$this->assertInstanceOf( $this->assertInstanceOf(
ActivityType::class, ActivityType::class,
$form->getData()['type'], $form->getData()['type'],
'The data is an instance of Chill\ActivityBundle\Entity\ActivityType' 'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType'
); );
$this->assertEquals($type->getId(), $form->getData()['type']->getId()); $this->assertEquals($type->getId(), $form->getData()['type']->getId());

View File

@@ -77,18 +77,6 @@ Choose a type: Choisir un type
4 hours: 4 heures 4 hours: 4 heures
4 hours 30: 4 heures 30 4 hours 30: 4 heures 30
5 hours: 5 heures 5 hours: 5 heures
5 hours 30: 5 heure 30
6 hours: 6 heures
6 hours 30: 6 heure 30
7 hours: 7 heures
7 hours 30: 7 heure 30
8 hours: 8 heures
8 hours 30: 8 heure 30
9 hours: 9 heures
9 hours 30: 9 heure 30
10 hours: 10 heures
11 hours: 11 heures
12 hours: 12 heures
Concerned groups: Parties concernées par l'échange Concerned groups: Parties concernées par l'échange
Persons in accompanying course: Usagers du parcours Persons in accompanying course: Usagers du parcours
Third persons: Tiers non-pro. Third persons: Tiers non-pro.
@@ -222,7 +210,6 @@ Documents label: Libellé du champ Documents
# activity type category admin # activity type category admin
ActivityTypeCategory list: Liste des catégories des types d'échange ActivityTypeCategory list: Liste des catégories des types d'échange
Create a new activity type category: Créer une nouvelle catégorie de type d'échange Create a new activity type category: Créer une nouvelle catégorie de type d'échange
Create a new activity in accompanying course: Créer un échange dans le parcours
# activity delete # activity delete
Remove activity: Supprimer un échange Remove activity: Supprimer un échange

View File

@@ -49,13 +49,13 @@
<li> <li>
<span> <span>
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr> <abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b> <b>{{ entity.createdBy|chill_entity_render_box }}</b>
</span> </span>
</li> </li>
<li> <li>
<span> <span>
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr> <abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b> <b>{{ entity.agent|chill_entity_render_box }}</b>
</span> </span>
</li> </li>

View File

@@ -18,10 +18,10 @@
<dd>{{ entity.type|chill_entity_render_box }}</dd> <dd>{{ entity.type|chill_entity_render_box }}</dd>
<dt class="inline">{{ 'Created by'|trans }}</dt> <dt class="inline">{{ 'Created by'|trans }}</dt>
<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd> <dd>{{ entity.createdBy }}</dd>
<dt class="inline">{{ 'Created for'|trans }}</dt> <dt class="inline">{{ 'Created for'|trans }}</dt>
<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd> <dd>{{ entity.agent }}</dd>
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt> <dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
{%- if entity.location.name is defined -%} {%- if entity.location.name is defined -%}

View File

@@ -72,21 +72,21 @@ days: jours
1 hour 30: 1 heure 30 1 hour 30: 1 heure 30
1 hour 45: 1 heure 45 1 hour 45: 1 heure 45
2 hours: 2 heures 2 hours: 2 heures
2 hours 30: 2 heures 30 2 hours 30: 2 heure 30
3 hours: 3 heures 3 hours: 3 heures
3 hours 30: 3 heures 30 3 hours 30: 3 heure 30
4 hours: 4 heures 4 hours: 4 heures
4 hours 30: 4 heures 30 4 hours 30: 4 heure 30
5 hours: 5 heures 5 hours: 5 heures
5 hours 30: 5 heures 30 5 hours 30: 5 heure 30
6 hours: 6 heures 6 hours: 6 heures
6 hours 30: 6 heures 30 6 hours 30: 6 heure 30
7 hours: 7 heures 7 hours: 7 heures
7 hours 30: 7 heures 30 7 hours 30: 7 heure 30
8 hours: 8 heures 8 hours: 8 heures
8 hours 30: 8 heures 30 8 hours 30: 8 heure 30
9 hours: 9 heures 9 hours: 9 heures
9 hours 30: 9 heures 30 9 hours 30: 9 heure 30
10 hours: 10 heures 10 hours: 10 heures
1/2 day: 1/2 jour 1/2 day: 1/2 jour
1 day: 1 jour 1 day: 1 jour

View File

@@ -440,16 +440,6 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
return $this->startDate; return $this->startDate;
} }
/**
* get the date of the calendar.
*
* Useful for showing the date of the calendar event, required by twig in some places.
*/
public function getDate(): ?\DateTimeImmutable
{
return $this->getStartDate();
}
public function getStatus(): ?string public function getStatus(): ?string
{ {
return $this->status; return $this->status;

View File

@@ -1,48 +0,0 @@
<?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\CalendarBundle\Menu;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security) {}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new calendar in accompanying course', [
'route' => 'chill_calendar_calendar_new',
'routeParameters' => [
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 20,
'icon' => 'plus',
])
;
}
}
}

View File

@@ -37,12 +37,12 @@ class RemoteEventConverter
* valid when the remote string contains also a timezone, like in * valid when the remote string contains also a timezone, like in
* lastModifiedDate. * lastModifiedDate.
*/ */
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P'; final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
/** /**
* Same as above, but sometimes the date is expressed with only 6 milliseconds. * Same as above, but sometimes the date is expressed with only 6 milliseconds.
*/ */
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP'; final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP';
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0'; private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';

View File

@@ -1,2 +1 @@
import './scss/badge.scss'; import './scss/badge.scss';
import './scss/calendar-list.scss';

View File

@@ -1,26 +0,0 @@
ul.calendar-list {
list-style-type: none;
padding: 0;
& > li {
display: inline-block;
}
& > li:nth-child(n+2) {
margin-left: 0.25rem;
}
}
div.calendar-list {
ul.calendar-list {
display: inline-block;
}
& > a.calendar-list__global {
display: inline-block;;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);
border-radius: 0.25rem;
text-align: center;
}
}

View File

@@ -55,7 +55,7 @@
<div class="item-col"> <div class="item-col">
<ul class="list-content"> <ul class="list-content">
{% if calendar.mainUser is not empty %} {% if calendar.mainUser is not empty %}
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span> <span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@@ -132,7 +132,7 @@
<li class="cancel"> <li class="cancel">
<span class="createdBy"> <span class="createdBy">
{{ 'Created by'|trans }} {{ 'Created by'|trans }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }} <b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
</span> </span>
</li> </li>
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %} {% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}

View File

@@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
switch ($attribute) { switch ($attribute) {
case self::SEE: case self::SEE:
case self::CREATE: case self::CREATE:
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) { if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
return false; return false;
} }

View File

@@ -26,7 +26,6 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim
From the day: Du From the day: Du
to the day: au to the day: au
Transform to activity: Transformer en échange Transform to activity: Transformer en échange
Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours
Will send SMS: Un SMS de rappel sera envoyé Will send SMS: Un SMS de rappel sera envoyé
Will not send SMS: Aucun SMS de rappel ne sera envoyé Will not send SMS: Aucun SMS de rappel ne sera envoyé
SMS already sent: Un SMS a été envoyé SMS already sent: Un SMS a été envoyé

View File

@@ -1,65 +0,0 @@
<?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\DocGeneratorBundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @template T of object
*/
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
{
public function testNullValueHasSameKeysAsNull(): void
{
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
foreach ($normalizedObject as $key => $value) {
if (in_array($key, ['isNull', 'type'])) {
continue;
}
if (is_array($value)) {
if (array_is_list($value)) {
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
} else {
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
}
} elseif (is_string($value)) {
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
}
}
}
/**
* @return T
*/
abstract public function provideNotNullObject(): object;
/**
* @return class-string<T>
*/
abstract public function provideDocGenExpectClass(): string;
abstract public function getNormalizer(): NormalizerInterface;
}

View File

@@ -1,46 +0,0 @@
<?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\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
class SignatureRequestController
{
public function __construct(
private MessageBusInterface $messageBus,
private StoredObjectManagerInterface $storedObjectManager,
) {}
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
public function processSignature(StoredObject $storedObject): Response
{
$content = $this->storedObjectManager->read($storedObject);
$this->messageBus->dispatch(new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'test signature',
'Mme Caroline Diallo',
$content
));
return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
}
}

View File

@@ -313,19 +313,4 @@ class StoredObject implements Document, TrackCreationInterface
return $this; return $this;
} }
public function saveHistory(): void
{
if ('' === $this->getFilename()) {
return;
}
$this->datas['history'][] = [
'filename' => $this->getFilename(),
'iv' => $this->getIv(),
'key_infos' => $this->getKeyInfos(),
'type' => $this->getType(),
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
];
}
} }

View File

@@ -57,8 +57,8 @@ class StoredObjectDataMapper implements DataMapperInterface
/** @var StoredObject $viewData */ /** @var StoredObject $viewData */
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
// we want to keep the previous history // we do not want to erase the previous object
$viewData->saveHistory(); $viewData = new StoredObject();
} }
$viewData->setFilename($forms['stored_object']->getData()['filename']); $viewData->setFilename($forms['stored_object']->getData()['filename']);

View File

@@ -4,13 +4,13 @@
Actions Actions
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'"> <li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button> <wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
</li> </li>
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined"> <li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button> <desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
</li> </li>
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'"> <li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button> <convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
</li> </li>
<li v-if="props.canDownload"> <li v-if="props.canDownload">

View File

@@ -13,7 +13,7 @@ import {reactive} from "vue";
import {StoredObject, StoredObjectCreated} from "../../types"; import {StoredObject, StoredObjectCreated} from "../../types";
interface ConvertButtonConfig { interface ConvertButtonConfig {
storedObject: StoredObject, storedObject: StoredObject|StoredObjectCreated,
classes: { [key: string]: boolean}, classes: { [key: string]: boolean},
filename?: string, filename?: string,
}; };

View File

@@ -11,7 +11,7 @@ import {build_wopi_editor_link} from "./helpers";
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types"; import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
interface WopiEditButtonConfig { interface WopiEditButtonConfig {
storedObject: StoredObject, storedObject: StoredObject|StoredObjectCreated,
returnPath?: string, returnPath?: string,
classes: {[k: string] : boolean}, classes: {[k: string] : boolean},
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,

View File

@@ -1,32 +0,0 @@
<?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\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
{
/**
* log prefix.
*/
private const P = '[pdf signed message] ';
public function __construct(
private LoggerInterface $logger,
) {}
public function __invoke(PdfSignedMessage $message): void
{
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
}
}

View File

@@ -1,66 +0,0 @@
<?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\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
/**
* Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
*/
final readonly class PdfSignedMessageSerializer implements SerializerInterface
{
public function decode(array $encodedEnvelope): Envelope
{
$body = $encodedEnvelope['body'];
try {
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
}
if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
}
$content = base64_decode($decoded['content'], true);
if (false === $content) {
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
}
$message = new PdfSignedMessage($decoded['signatureId'], $content);
return new Envelope($message);
}
public function encode(Envelope $envelope): array
{
$message = $envelope->getMessage();
if (!$message instanceof PdfSignedMessage) {
throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
}
$data = [
'signatureId' => $message->signatureId,
'content' => base64_encode($message->content),
];
return [
'body' => json_encode($data, JSON_THROW_ON_ERROR),
'headers' => [],
];
}
}

View File

@@ -1,29 +0,0 @@
<?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\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
/**
* Message which is sent when we request a signature on a pdf.
*/
final readonly class RequestPdfSignMessage
{
public function __construct(
public int $signatureId,
public PDFSignatureZone $PDFSignatureZone,
public int $signatureZoneIndex,
public string $reason,
public string $signerText,
public string $content,
) {}
}

View File

@@ -1,105 +0,0 @@
<?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\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Serialize a RequestPdfSignMessage, for external consumer.
*/
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
{
public function __construct(
private NormalizerInterface $normalizer,
private DenormalizerInterface $denormalizer,
) {}
public function decode(array $encodedEnvelope): Envelope
{
$body = $encodedEnvelope['body'];
$headers = $encodedEnvelope['headers'];
if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
throw new MessageDecodingFailedException('serializer does not support this message');
}
$data = json_decode($body, true);
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
AbstractNormalizer::GROUPS => ['write'],
]);
$content = base64_decode($data['content'], true);
if (false === $content) {
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
}
$message = new RequestPdfSignMessage(
$data['signatureId'],
$zoneSignature,
$data['signatureZoneIndex'],
$data['reason'],
$data['signerText'],
$content,
);
// in case of redelivery, unserialize any stamps
$stamps = [];
if (isset($headers['stamps'])) {
$stamps = unserialize($headers['stamps']);
}
return new Envelope($message, $stamps);
}
public function encode(Envelope $envelope): array
{
$message = $envelope->getMessage();
if (!$message instanceof RequestPdfSignMessage) {
throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
}
$data = [
'signatureId' => $message->signatureId,
'signatureZoneIndex' => $message->signatureZoneIndex,
'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
'reason' => $message->reason,
'signerText' => $message->signerText,
'content' => base64_encode($message->content),
];
$allStamps = [];
foreach ($envelope->all() as $stamp) {
if ($stamp instanceof NonSendableStampInterface) {
continue;
}
$allStamps = [...$allStamps, ...$stamp];
}
return [
'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
'headers' => [
'stamps' => serialize($allStamps),
'Message' => RequestPdfSignMessage::class,
],
];
}
}

View File

@@ -1,33 +0,0 @@
<?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\DocStoreBundle\Service\Signature;
use Symfony\Component\Serializer\Annotation\Groups;
final readonly class PDFPage
{
public function __construct(
#[Groups(['read'])]
public int $index,
#[Groups(['read'])]
public float $width,
#[Groups(['read'])]
public float $height,
) {}
public function equals(self $page): bool
{
return $page->index === $this->index
&& round($page->width, 2) === round($this->width, 2)
&& round($page->height, 2) === round($this->height, 2);
}
}

View File

@@ -1,40 +0,0 @@
<?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\DocStoreBundle\Service\Signature;
use Symfony\Component\Serializer\Annotation\Groups;
final readonly class PDFSignatureZone
{
public function __construct(
#[Groups(['read'])]
public float $x,
#[Groups(['read'])]
public float $y,
#[Groups(['read'])]
public float $height,
#[Groups(['read'])]
public float $width,
#[Groups(['read'])]
public PDFPage $PDFPage,
) {}
public function equals(self $other): bool
{
return
$this->x == $other->x
&& $this->y == $other->y
&& $this->height == $other->height
&& $this->width == $other->width
&& $this->PDFPage->equals($other->PDFPage);
}
}

View File

@@ -1,58 +0,0 @@
<?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\DocStoreBundle\Service\Signature;
use Smalot\PdfParser\Parser;
class PDFSignatureZoneParser
{
public const ZONE_SIGNATURE_START = 'signature_zone';
private Parser $parser;
public function __construct(
public float $defaultHeight = 180.0,
public float $defaultWidth = 180.0,
) {
$this->parser = new Parser();
}
/**
* @return list<PDFSignatureZone>
*/
public function findSignatureZones(string $fileContent): array
{
$pdf = $this->parser->parseContent($fileContent);
$zones = [];
$defaults = $pdf->getObjectsByType('Pages');
$defaultPage = reset($defaults);
$defaultPageDetails = $defaultPage->getDetails();
foreach ($pdf->getPages() as $index => $page) {
$details = $page->getDetails();
$pdfPage = new PDFPage(
$index,
(float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
(float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
);
foreach ($page->getDataTm() as $dataTm) {
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
$zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
}
}
}
return $zones;
}
}

View File

@@ -1,53 +0,0 @@
<?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\DocStoreBundle\Tests\Entity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectTest extends KernelTestCase
{
public function testSaveHistory(): void
{
$storedObject = new StoredObject();
$storedObject
->setFilename('test_0')
->setIv([2, 4, 6, 8])
->setKeyInfos(['key' => ['data0' => 'data0']])
->setType('text/html');
$storedObject->saveHistory();
$storedObject
->setFilename('test_1')
->setIv([8, 10, 12])
->setKeyInfos(['key' => ['data1' => 'data1']])
->setType('text/text');
$storedObject->saveHistory();
self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']);
self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']);
self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']);
self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']);
self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']);
self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']);
self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']);
self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']);
}
}

View File

@@ -56,14 +56,14 @@ class StoredObjectTypeTest extends TypeTestCase
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"} {"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
JSON]; JSON];
$model = new StoredObject(); $model = new StoredObject();
$originalObjectId = spl_object_hash($model); $originalObjectId = spl_object_id($model);
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]); $form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
$form->submit($formData); $form->submit($formData);
$this->assertTrue($form->isSynchronized()); $this->assertTrue($form->isSynchronized());
$model = $form->getData(); $model = $form->getData();
$this->assertEquals($originalObjectId, spl_object_hash($model)); $this->assertNotEquals($originalObjectId, spl_object_hash($model));
$this->assertEquals('abcdef', $model->getFilename()); $this->assertEquals('abcdef', $model->getFilename());
$this->assertEquals([10, 15, 20, 30], $model->getIv()); $this->assertEquals([10, 15, 20, 30], $model->getIv());
$this->assertEquals('text/html', $model->getType()); $this->assertEquals('text/html', $model->getType());

View File

@@ -1,63 +0,0 @@
<?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\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
/**
* @internal
*
* @coversNothing
*/
class PdfSignedMessageSerializerTest extends TestCase
{
public function testDecode(): void
{
$asString = <<<'JSON'
{"signatureId": 0, "content": "dGVzdAo="}
JSON;
$actual = $this->buildSerializer()->decode(['body' => $asString]);
self::assertInstanceOf(Envelope::class, $actual);
$message = $actual->getMessage();
self::assertInstanceOf(PdfSignedMessage::class, $message);
self::assertEquals("test\n", $message->content);
self::assertEquals(0, $message->signatureId);
}
public function testEncode(): void
{
$envelope = new Envelope(
new PdfSignedMessage(0, "test\n")
);
$actual = $this->buildSerializer()->encode($envelope);
self::assertIsArray($actual);
self::assertArrayHasKey('body', $actual);
self::assertArrayHasKey('headers', $actual);
self::assertEquals([], $actual['headers']);
self::assertEquals(<<<'JSON'
{"signatureId":0,"content":"dGVzdAo="}
JSON, $actual['body']);
}
private function buildSerializer(): PdfSignedMessageSerializer
{
return new PdfSignedMessageSerializer();
}
}

View File

@@ -1,137 +0,0 @@
<?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\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @internal
*
* @coversNothing
*/
class RequestPdfSignMessageSerializerTest extends TestCase
{
public function testEncode(): void
{
$serializer = $this->buildSerializer();
$envelope = new Envelope(
$request = new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'metadata to add to the signature',
'Mme Caroline Diallo',
'abc'
),
);
$actual = $serializer->encode($envelope);
$expectedBody = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'signerText' => $request->signerText,
'content' => base64_encode($request->content),
]);
self::assertIsArray($actual);
self::assertArrayHasKey('body', $actual);
self::assertArrayHasKey('headers', $actual);
self::assertEquals($expectedBody, $actual['body']);
}
public function testDecode(): void
{
$serializer = $this->buildSerializer();
$request = new RequestPdfSignMessage(
0,
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0,
'metadata to add to the signature',
'Mme Caroline Diallo',
'abc'
);
$bodyAsString = json_encode([
'signatureId' => $request->signatureId,
'signatureZoneIndex' => $request->signatureZoneIndex,
'signatureZone' => ['x' => 10.0],
'reason' => $request->reason,
'signerText' => $request->signerText,
'content' => base64_encode($request->content),
], JSON_THROW_ON_ERROR);
$actual = $serializer->decode([
'body' => $bodyAsString,
'headers' => [
'Message' => RequestPdfSignMessage::class,
],
]);
self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
self::assertEquals($request->reason, $actual->getMessage()->reason);
self::assertEquals($request->signerText, $actual->getMessage()->signerText);
self::assertEquals($request->content, $actual->getMessage()->content);
self::assertNotNull($actual->getMessage()->PDFSignatureZone);
}
private function buildSerializer(): RequestPdfSignMessageSerializer
{
$normalizer =
new class () implements NormalizerInterface {
public function normalize($object, ?string $format = null, array $context = []): array
{
if (!$object instanceof PDFSignatureZone) {
throw new UnexpectedValueException('expected RequestPdfSignMessage');
}
return [
'x' => $object->x,
];
}
public function supportsNormalization($data, ?string $format = null): bool
{
return $data instanceof PDFSignatureZone;
}
};
$denormalizer = new class () implements DenormalizerInterface {
public function denormalize($data, string $type, ?string $format = null, array $context = [])
{
return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
}
public function supportsDenormalization($data, string $type, ?string $format = null)
{
return PDFSignatureZone::class === $type;
}
};
$serializer = new Serializer([$normalizer, $denormalizer]);
return new RequestPdfSignMessageSerializer($serializer, $serializer);
}
}

View File

@@ -1,77 +0,0 @@
<?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 Tests\Service\Signature;
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use PHPUnit\Framework\TestCase;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
/**
* @internal
*
* @coversNothing
*/
class PDFSignatureZoneParserTest extends TestCase
{
private static PDFSignatureZoneParser $parser;
public static function setUpBeforeClass(): void
{
self::$parser = new PDFSignatureZoneParser();
}
/**
* @dataProvider provideFiles
*
* @param list<PDFSignatureZone> $expected
*/
public function testFindSignatureZones(string $filePath, array $expected): void
{
$content = file_get_contents($filePath);
if (false === $content) {
throw new \LogicException("Unable to read file {$filePath}");
}
$actual = self::$parser->findSignatureZones($content);
self::assertEquals(count($expected), count($actual));
foreach ($actual as $index => $signatureZone) {
self::assertObjectEquals($expected[$index], $signatureZone);
}
}
public static function provideFiles(): iterable
{
yield [
__DIR__.'/data/signature_2_signature_page_1.pdf',
[
new PDFSignatureZone(
127.7,
95.289,
180.0,
180.0,
$page = new PDFPage(0, 595.30393, 841.8897)
),
new PDFSignatureZone(
269.5,
95.289,
180.0,
180.0,
$page,
),
],
];
}
}

View File

@@ -121,4 +121,9 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
{ {
return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
} }
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return false;
}
} }

View File

@@ -632,7 +632,7 @@ class ExportController extends AbstractController
} }
} }
private function rebuildRawData(?string $key): array private function rebuildRawData(string $key): array
{ {
if (null === $key) { if (null === $key) {
throw $this->createNotFoundException('key does not exists'); throw $this->createNotFoundException('key does not exists');

View File

@@ -61,6 +61,8 @@ final class PermissionsGroupController extends AbstractController
$form = $this->createAddRoleScopeForm($permissionsGroup); $form = $this->createAddRoleScopeForm($permissionsGroup);
$form->handleRequest($request); $form->handleRequest($request);
dump($form->isSubmitted());
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$roleScope = $this->getPersistentRoleScopeBy( $roleScope = $this->getPersistentRoleScopeBy(
$form['composed_role_scope']->getData()->getRole(), $form['composed_role_scope']->getData()->getRole(),

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
class UserGroupApiController extends ApiController {}

View File

@@ -13,16 +13,15 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Form\EntityWorkflowCommentType;
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
use Chill\MainBundle\Form\WorkflowStepType; use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Security\ChillSecurity; use Chill\MainBundle\Security\ChillSecurity;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
@@ -277,11 +276,10 @@ class WorkflowController extends AbstractController
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$errors = []; $errors = [];
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition // possible transition
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$usersInvolved = $entityWorkflow->getUsersInvolved(); $usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true); $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
@@ -291,8 +289,9 @@ class WorkflowController extends AbstractController
$transitionForm = $this->createForm( $transitionForm = $this->createForm(
WorkflowStepType::class, WorkflowStepType::class,
$stepDTO, $entityWorkflow->getCurrentStep(),
[ [
'transition' => true,
'entity_workflow' => $entityWorkflow, 'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved, 'suggested_users' => $usersInvolved,
] ]
@@ -311,7 +310,12 @@ class WorkflowController extends AbstractController
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs))); throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
} }
$workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]); // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
$workflow->apply($entityWorkflow, $transition);
$this->entityManager->flush(); $this->entityManager->flush();
@@ -323,6 +327,22 @@ class WorkflowController extends AbstractController
} }
} }
/*
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
$commentForm->handleRequest($request);
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
$this->entityManager->persist($newComment);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
*/
return $this->render( return $this->render(
'@ChillMain/Workflow/index.html.twig', '@ChillMain/Workflow/index.html.twig',
[ [
@@ -332,7 +352,7 @@ class WorkflowController extends AbstractController
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null, 'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow, 'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors, 'transition_form_errors' => $errors,
'signatures' => $signatures, // 'comment_form' => $commentForm->createView(),
] ]
); );
} }
@@ -351,47 +371,4 @@ class WorkflowController extends AbstractController
return $lines; return $lines;
} }
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
public function addSignatureMetadata(int $signature_id, Request $request): Response
{
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('signature_route_user');
}
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
$metadataForm->handleRequest($request);
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
$data = $metadataForm->getData();
$signature->setSignatureMetadata(
[
'base_signer' => [
'document_type' => $data['documentType'],
'document_number' => $data['documentNumber'],
'expiration_date' => $data['expirationDate'],
],
]
);
$this->entityManager->persist($signature);
$this->entityManager->flush();
// Todo should redirect to document for actual signing? To be adjusted still
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
}
return $this->render(
'@ChillMain/Workflow/_signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),
]
);
}
} }

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
class LoadUserGroup extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['user-group'];
}
public function load(ObjectManager $manager)
{
$centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
$centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);
$multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']);
$administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']);
$administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']);
$level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level');
$level1->addUser($centerASocial)->addUser($centerBSocial);
$manager->persist($level1);
$level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level');
$level2->addUser($multiCenter);
$manager->persist($level2);
$level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level');
$level3->addUser($multiCenter);
$manager->persist($level3);
$tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', '');
$tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial);
$manager->persist($tss);
$admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', '');
$admins->addUser($administrativeA)->addUser($administrativeB);
$manager->persist($admins);
$manager->flush();
}
private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup
{
$userGroup = new UserGroup();
return $userGroup
->setLabel(['fr' => $title])
->setBackgroundColor($backgroundColor)
->setForegroundColor($foregroundColor)
->setExcludeKey($excludeKey)
;
}
}

View File

@@ -24,6 +24,7 @@ use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\NewsItemController;
use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserGroupApiController;
use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController; use Chill\MainBundle\Controller\UserJobController;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
@@ -59,6 +60,7 @@ use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\CenterType; use Chill\MainBundle\Form\CenterType;
use Chill\MainBundle\Form\CivilityType; use Chill\MainBundle\Form\CivilityType;
@@ -193,11 +195,6 @@ class ChillMainExtension extends Extension implements
[] []
); );
$container->setParameter(
'chill_main.workflow_signatures.base_signer.document_kinds',
$config['workflow_signature']['base_signer']['document_kinds']
);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml'); $loader->load('services.yaml');
$loader->load('services/doctrine.yaml'); $loader->load('services/doctrine.yaml');
@@ -808,6 +805,21 @@ class ChillMainExtension extends Extension implements
], ],
], ],
], ],
[
'class' => UserGroup::class,
'controller' => UserGroupApiController::class,
'name' => 'user-group',
'base_path' => '/api/1.0/main/user-group',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
],
], ],
]); ]);
} }

View File

@@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface
$defaultTransporter = new Reference(NullShortMessageSender::class); $defaultTransporter = new Reference(NullShortMessageSender::class);
} elseif ('ovh' === $dsn['scheme']) { } elseif ('ovh' === $dsn['scheme']) {
if (!class_exists('\\'.\Ovh\Api::class)) { if (!class_exists('\\'.\Ovh\Api::class)) {
throw new RuntimeException('Class \Ovh\Api not found'); throw new RuntimeException('Class \\Ovh\\Api not found');
} }
foreach (['user', 'host', 'pass'] as $component) { foreach (['user', 'host', 'pass'] as $component) {

View File

@@ -277,32 +277,6 @@ class Configuration implements ConfigurationInterface
->end() // end of root ->end() // end of root
; ;
$rootNode->children()
->arrayNode('workflow_signature')
->children()
->arrayNode('base_signer')
->children()
->arrayNode('document_kinds')
->arrayPrototype()
->children()
->scalarNode('key')->cannotBeEmpty()->end()
->arrayNode('labels')
->arrayPrototype()
->children()
->scalarNode('lang')->cannotBeEmpty()->end()
->scalarNode('label')->cannotBeEmpty()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();
$rootNode->children() $rootNode->children()
->arrayNode('add_address')->addDefaultsIfNotSet()->children() ->arrayNode('add_address')->addDefaultsIfNotSet()->children()
->scalarNode('default_country')->cannotBeEmpty()->defaultValue('BE')->end() ->scalarNode('default_country')->cannotBeEmpty()->defaultValue('BE')->end()

View File

@@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->mainLocation; return $this->mainLocation;
} }
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
{ {
$atDate ??= new \DateTimeImmutable('now'); $at ??= new \DateTimeImmutable('now');
foreach ($this->scopeHistories as $scopeHistory) { foreach ($this->scopeHistories as $scopeHistory) {
if ($atDate >= $scopeHistory->getStartDate() && ( if ($at >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate() null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
)) { )) {
return $scopeHistory->getScope(); return $scopeHistory->getScope();
} }
@@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->salt; return $this->salt;
} }
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
{ {
$atDate ??= new \DateTimeImmutable('now'); $at ??= new \DateTimeImmutable('now');
foreach ($this->jobHistories as $jobHistory) { foreach ($this->jobHistories as $jobHistory) {
if ($atDate >= $jobHistory->getStartDate() && ( if ($at >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate() null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
)) { )) {
return $jobHistory->getJob(); return $jobHistory->getJob();
} }
@@ -285,11 +285,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this->jobHistories; return $this->jobHistories;
} }
public function getUserScopeHistories(): Collection
{
return $this->scopeHistories;
}
/** /**
* @return ArrayCollection|UserJobHistory[] * @return ArrayCollection|UserJobHistory[]
*/ */

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_user_group')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['user_group' => UserGroup::class])]
class UserGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
#[Serializer\Groups(['read'])]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
#[Serializer\Groups(['read'])]
private array $label = [];
/**
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
private Collection $users;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
#[Serializer\Groups(['read'])]
private string $backgroundColor = '#ffffffff';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
#[Serializer\Groups(['read'])]
private string $foregroundColor = '#000000ff';
/**
* Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship
* will exclude others.
*
* An empty string means "no exclusion"
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
#[Serializer\Groups(['read'])]
private string $excludeKey = '';
public function __construct()
{
$this->users = new ArrayCollection();
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
}
return $this;
}
public function removeUser(User $user): self
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
}
return $this;
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
public function getUsers(): Collection
{
return $this->users;
}
public function getForegroundColor(): string
{
return $this->foregroundColor;
}
public function getExcludeKey(): string
{
return $this->excludeKey;
}
public function getBackgroundColor(): string
{
return $this->backgroundColor;
}
public function setForegroundColor(string $foregroundColor): self
{
$this->foregroundColor = $foregroundColor;
return $this;
}
public function setBackgroundColor(string $backgroundColor): self
{
$this->backgroundColor = $backgroundColor;
return $this;
}
public function setExcludeKey(string $excludeKey): self
{
$this->excludeKey = $excludeKey;
return $this;
}
public function setLabel(array $label): self
{
$this->label = $label;
return $this;
}
}

View File

@@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation; use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@@ -34,6 +34,35 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
use TrackUpdateTrait; use TrackUpdateTrait;
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*
* This is in used in order to let controller inform who will be the future emails which will validate
* the next step. This is necessary to perform some computation about the next emails, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|string[]
*/
public array $futureDestEmails = [];
/**
* a list of future dest users for the next steps.
*
* This is in used in order to let controller inform who will be the future users which will validate
* the next step. This is necessary to perform some computation about the next users, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|User[]
*/
public array $futureDestUsers = [];
/** /**
* @var Collection<EntityWorkflowComment> * @var Collection<EntityWorkflowComment>
*/ */
@@ -413,23 +442,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
* *
* @return $this * @return $this
*/ */
public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self public function setStep(string $step): self
{ {
$newStep = new EntityWorkflowStep(); $newStep = new EntityWorkflowStep();
$newStep->setCurrentStep($step); $newStep->setCurrentStep($step);
foreach ($transitionContextDTO->futureCcUsers as $user) {
$newStep->addCcUser($user);
}
foreach ($transitionContextDTO->futureDestUsers as $user) {
$newStep->addDestUser($user);
}
foreach ($transitionContextDTO->futureDestEmails as $email) {
$newStep->addDestEmail($email);
}
// copy the freeze // copy the freeze
if ($this->isFreeze()) { if ($this->isFreeze()) {
$newStep->setFreezeAfter(true); $newStep->setFreezeAfter(true);

View File

@@ -42,25 +42,19 @@ class EntityWorkflowStep
private array $destEmail = []; private array $destEmail = [];
/** /**
* @var Collection<int, User> * @var Collection<User>
*/ */
#[ORM\ManyToMany(targetEntity: User::class)] #[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
private Collection $destUser; private Collection $destUser;
/** /**
* @var Collection<int, User> * @var Collection<User>
*/ */
#[ORM\ManyToMany(targetEntity: User::class)] #[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
private Collection $destUserByAccessKey; private Collection $destUserByAccessKey;
/**
* @var Collection <int, EntityWorkflowStepSignature>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $signatures;
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
private ?EntityWorkflow $entityWorkflow = null; private ?EntityWorkflow $entityWorkflow = null;
@@ -103,7 +97,6 @@ class EntityWorkflowStep
$this->ccUser = new ArrayCollection(); $this->ccUser = new ArrayCollection();
$this->destUser = new ArrayCollection(); $this->destUser = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection();
$this->signatures = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
} }
@@ -143,29 +136,6 @@ class EntityWorkflowStep
return $this; return $this;
} }
/**
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
*/
public function addSignature(EntityWorkflowStepSignature $signature): self
{
if (!$this->signatures->contains($signature)) {
$this->signatures[] = $signature;
}
return $this;
}
public function removeSignature(EntityWorkflowStepSignature $signature): self
{
if ($this->signatures->contains($signature)) {
$this->signatures->removeElement($signature);
}
$signature->detachEntityWorkflowStep();
return $this;
}
public function getAccessKey(): string public function getAccessKey(): string
{ {
return $this->accessKey; return $this->accessKey;
@@ -228,14 +198,6 @@ class EntityWorkflowStep
return $this->entityWorkflow; return $this->entityWorkflow;
} }
/**
* @return Collection<int, EntityWorkflowStepSignature>
*/
public function getSignatures(): Collection
{
return $this->signatures;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;

View File

@@ -1,156 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Entity\Workflow;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true)]
private ?User $userSigner = null;
#[ORM\ManyToOne(targetEntity: Person::class)]
#[ORM\JoinColumn(nullable: true)]
private ?Person $personSigner = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
private ?\DateTimeImmutable $stateDate = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
private array $signatureMetadata = [];
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
private ?int $zoneSignatureIndex = null;
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
#[ORM\JoinColumn(nullable: false)]
private ?EntityWorkflowStep $step = null;
public function __construct(
EntityWorkflowStep $step,
User|Person $signer,
) {
$this->step = $step;
$step->addSignature($this);
$this->setSigner($signer);
}
private function setSigner(User|Person $signer): void
{
if ($signer instanceof User) {
$this->userSigner = $signer;
} else {
$this->personSigner = $signer;
}
}
public function getId(): ?int
{
return $this->id;
}
public function getStep(): EntityWorkflowStep
{
return $this->step;
}
public function getSigner(): User|Person
{
if (null !== $this->userSigner) {
return $this->userSigner;
}
return $this->personSigner;
}
public function getSignatureMetadata(): array
{
return $this->signatureMetadata;
}
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
{
$this->signatureMetadata = $signatureMetadata;
return $this;
}
public function getState(): EntityWorkflowSignatureStateEnum
{
return $this->state;
}
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
{
$this->state = $state;
return $this;
}
public function getStateDate(): ?\DateTimeImmutable
{
return $this->stateDate;
}
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
{
$this->stateDate = $stateDate;
return $this;
}
public function getZoneSignatureIndex(): ?int
{
return $this->zoneSignatureIndex;
}
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
{
$this->zoneSignatureIndex = $zoneSignatureIndex;
return $this;
}
/**
* Detach from the @see{EntityWorkflowStep}.
*
* @internal used internally to remove the current signature
*
* @return $this
*/
public function detachEntityWorkflowStep(): self
{
$this->step = null;
return $this;
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
/**
* Transform data from filter.
*
* This interface defines a method for transforming filter's form data before it is processed.
*
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
* and replace it with some default values, or new default values.
*/
interface DataTransformerInterface
{
public function transformData(?array $before): array;
}

View File

@@ -190,7 +190,7 @@ class ExportManager
// throw an error if the export require other modifier, which is // throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery` // not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) { if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`'); throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`');
} }
} elseif ($query instanceof QueryBuilder) { } elseif ($query instanceof QueryBuilder) {
// handle filters // handle filters
@@ -203,7 +203,7 @@ class ExportManager
'dql' => $query->getDQL(), 'dql' => $query->getDQL(),
]); ]);
} else { } else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.'); throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.');
} }
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);

View File

@@ -32,9 +32,6 @@ interface FilterInterface extends ModifierInterface
/** /**
* Get the default data, that can be use as "data" for the form. * Get the default data, that can be use as "data" for the form.
*
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
* transforme the filters's data saved in an export to the desired state.
*/ */
public function getFormDefaultData(): array; public function getFormDefaultData(): array;

View File

@@ -11,9 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export; namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -21,7 +19,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AggregatorType extends AbstractType class AggregatorType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options): void public function __construct() {}
public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$exportManager = $options['export_manager']; $exportManager = $options['export_manager'];
$aggregator = $exportManager->getAggregator($options['aggregator_alias']); $aggregator = $exportManager->getAggregator($options['aggregator_alias']);
@@ -32,24 +32,17 @@ class AggregatorType extends AbstractType
'required' => false, 'required' => false,
]); ]);
$aggregatorFormBuilder = $builder->create('form', FormType::class, [ $filterFormBuilder = $builder->create('form', FormType::class, [
'compound' => true, 'compound' => true,
'required' => false, 'required' => false,
'error_bubbling' => false, 'error_bubbling' => false,
]); ]);
$aggregator->buildForm($aggregatorFormBuilder); $aggregator->buildForm($filterFormBuilder);
if ($aggregator instanceof DataTransformerInterface) { $builder->add($filterFormBuilder);
$aggregatorFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $aggregator->transformData($data),
));
} }
$builder->add($aggregatorFormBuilder); public function configureOptions(OptionsResolver $resolver)
}
public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setRequired('aggregator_alias') $resolver->setRequired('aggregator_alias')
->setRequired('export_manager') ->setRequired('export_manager')

View File

@@ -11,10 +11,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export; namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Export\FilterInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -43,13 +41,6 @@ class FilterType extends AbstractType
]); ]);
$filter->buildForm($filterFormBuilder); $filter->buildForm($filterFormBuilder);
if ($filter instanceof DataTransformerInterface) {
$filterFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $filter->transformData($data),
));
}
$builder->add($filterFormBuilder); $builder->add($filterFormBuilder);
} }

View File

@@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class WorkflowSignatureMetadataType extends AbstractType
{
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$documentTypeChoices = $this->parameterBag->get('chill_main.id_document_kinds');
$choices = [];
foreach ($documentTypeChoices as $documentType) {
$labels = [];
foreach ($documentType['labels'] as $label) {
$labels[$label['lang']] = $label['label'];
}
$localizedLabel = $this->translatableStringHelper->localize($labels);
if (null !== $localizedLabel) {
$choices[$localizedLabel] = $documentType['key'];
}
}
$builder
->add('documentType', ChoiceType::class, [
'label' => 'workflow.signature_zone.metadata.docType',
'expanded' => false,
'required' => true,
'choices' => $choices,
])
->add('documentNumber', TextType::class, [
'required' => true,
'label' => 'workflow.signature_zone.metadata.docNumber',
])
->add('expirationDate', ChillDateType::class, [
'required' => true,
'input' => 'datetime_immutable',
'label' => 'workflow.signature_zone.metadata.docExpiration',
]);
}
}

View File

@@ -12,12 +12,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form; namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -32,19 +34,18 @@ use Symfony\Component\Workflow\Transition;
class WorkflowStepType extends AbstractType class WorkflowStepType extends AbstractType
{ {
public function __construct( public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
private readonly Registry $registry,
private readonly TranslatableStringHelperInterface $translatableStringHelper
) {}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
/** @var EntityWorkflow $entityWorkflow */ /** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $options['entity_workflow']; $entityWorkflow = $options['entity_workflow'];
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$place = $workflow->getMarking($entityWorkflow); $place = $workflow->getMarking($entityWorkflow);
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]); $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
if (true === $options['transition']) {
if (null === $options['entity_workflow']) { if (null === $options['entity_workflow']) {
throw new \LogicException('if transition is true, entity_workflow should be defined'); throw new \LogicException('if transition is true, entity_workflow should be defined');
} }
@@ -130,20 +131,23 @@ class WorkflowStepType extends AbstractType
]; ];
}, },
]) ])
->add('futureDestUsers', PickUserDynamicType::class, [ ->add('future_dest_users', PickUserDynamicType::class, [
'label' => 'workflow.dest for next steps', 'label' => 'workflow.dest for next steps',
'multiple' => true, 'multiple' => true,
'mapped' => false,
'suggested' => $options['suggested_users'], 'suggested' => $options['suggested_users'],
]) ])
->add('futureCcUsers', PickUserDynamicType::class, [ ->add('future_cc_users', PickUserDynamicType::class, [
'label' => 'workflow.cc for next steps', 'label' => 'workflow.cc for next steps',
'multiple' => true, 'multiple' => true,
'mapped' => false,
'required' => false, 'required' => false,
'suggested' => $options['suggested_users'], 'suggested' => $options['suggested_users'],
]) ])
->add('futureDestEmails', ChillCollectionType::class, [ ->add('future_dest_emails', ChillCollectionType::class, [
'label' => 'workflow.dest by email', 'label' => 'workflow.dest by email',
'help' => 'workflow.dest by email help', 'help' => 'workflow.dest by email help',
'mapped' => false,
'allow_add' => true, 'allow_add' => true,
'entry_type' => EmailType::class, 'entry_type' => EmailType::class,
'button_add_label' => 'workflow.Add an email', 'button_add_label' => 'workflow.Add an email',
@@ -156,27 +160,43 @@ class WorkflowStepType extends AbstractType
'label' => 'Email', 'label' => 'Email',
], ],
]); ]);
}
if (
$handler->supportsFreeze($entityWorkflow)
&& !$entityWorkflow->isFreeze()
) {
$builder
->add('freezeAfter', CheckboxType::class, [
'required' => false,
'label' => 'workflow.Freeze',
'help' => 'workflow.The associated element will be freezed',
]);
}
$builder $builder
->add('comment', ChillTextareaType::class, [ ->add('comment', ChillTextareaType::class, [
'required' => false, 'required' => false,
'label' => 'Comment', 'label' => 'Comment',
'empty_data' => '',
]); ]);
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver $resolver
->setDefault('data_class', WorkflowTransitionContextDTO::class) ->setDefined('class')
->setRequired('transition')
->setAllowedTypes('transition', 'bool')
->setRequired('entity_workflow') ->setRequired('entity_workflow')
->setAllowedTypes('entity_workflow', EntityWorkflow::class) ->setAllowedTypes('entity_workflow', EntityWorkflow::class)
->setDefault('suggested_users', []) ->setDefault('suggested_users', [])
->setDefault('constraints', [ ->setDefault('constraints', [
new Callback( new Callback(
function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) { function ($step, ExecutionContextInterface $context, $payload) {
$workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName()); /** @var EntityWorkflowStep $step */
$transition = $step->transition; $form = $context->getObject();
$workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
$transition = $form['transition']->getData();
$toFinal = true; $toFinal = true;
if (null === $transition) { if (null === $transition) {
@@ -192,8 +212,8 @@ class WorkflowStepType extends AbstractType
$toFinal = false; $toFinal = false;
} }
} }
$destUsers = $step->futureDestUsers; $destUsers = $form['future_dest_users']->getData();
$destEmails = $step->futureDestEmails; $destEmails = $form['future_dest_emails']->getData();
if (!$toFinal && [] === $destUsers && [] === $destEmails) { if (!$toFinal && [] === $destUsers && [] === $destEmails) {
$context $context
@@ -204,6 +224,20 @@ class WorkflowStepType extends AbstractType
} }
} }
), ),
new Callback(
function ($step, ExecutionContextInterface $context, $payload) {
$form = $context->getObject();
foreach ($form->get('future_dest_users')->getData() as $u) {
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
$context
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
->atPath('ccUsers')
->addViolation();
}
}
}
),
]); ]);
} }
} }

View File

@@ -76,6 +76,24 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']); ->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
} }
/**
* @throws NumberParseException
*/
public function parse(string $phoneNumber): PhoneNumber
{
$sanitizedPhoneNumber = $phoneNumber;
if (str_starts_with($sanitizedPhoneNumber, '00')) {
$sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
}
if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
$sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
}
return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
}
/** /**
* Get type (mobile, landline, ...) for phone number. * Get type (mobile, landline, ...) for phone number.
*/ */

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Repository\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectRepository;
/**
* @template-implements ObjectRepository<EntityWorkflowStepSignature>
*/
class EntityWorkflowStepSignatureRepository implements ObjectRepository
{
private \Doctrine\ORM\EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class);
}
public function find($id): ?EntityWorkflowStepSignature
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?EntityWorkflowStepSignature
{
return $this->findOneBy($criteria);
}
public function getClassName(): string
{
return EntityWorkflowStepSignature::class;
}
}

View File

@@ -59,10 +59,6 @@ export const ISOToDatetime = (str: string|null): Date|null => {
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s)); [hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
; ;
if ('0000' === timezone) {
return new Date(Date.UTC(year, month-1, date, hours, minutes, seconds));
}
return new Date(year, month-1, date, hours, minutes, seconds); return new Date(year, month-1, date, hours, minutes, seconds);
} }

View File

@@ -15,9 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import mime from 'mime'; var mime = require('mime')
export const download_report = (url, container) => { var download_report = (url, container) => {
var download_text = container.dataset.downloadText, var download_text = container.dataset.downloadText,
alias = container.dataset.alias; alias = container.dataset.alias;
@@ -43,14 +43,7 @@ export const download_report = (url, container) => {
content = URL.createObjectURL(blob); content = URL.createObjectURL(blob);
} }
const extensions = new Map(); extension = mime.getExtension(type);
extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx');
extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods');
extensions.set('application/vnd.ms-excel', 'xlsx');
extensions.set('text/csv', 'csv');
extensions.set('text/csv; charset=utf-8', 'csv');
extension = extensions.get(type);
link.appendChild(document.createTextNode(download_text)); link.appendChild(document.createTextNode(download_text));
link.classList.add("btn", "btn-action"); link.classList.add("btn", "btn-action");
@@ -62,7 +55,7 @@ export const download_report = (url, container) => {
container.innerHTML = ""; container.innerHTML = "";
container.appendChild(link); container.appendChild(link);
}).catch(function(error) { }).catch(function(error) {
console.error(error); console.log(error);
var problem_text = var problem_text =
document.createTextNode("Problem during download"); document.createTextNode("Problem during download");
@@ -70,3 +63,5 @@ export const download_report = (url, container) => {
.replaceChild(problem_text, container.firstChild); .replaceChild(problem_text, container.firstChild);
}); });
}; };
module.exports = download_report;

View File

@@ -39,5 +39,23 @@ ClassicEditor.defaultConfig = {
'redo' 'redo'
] ]
}, },
language: 'fr', language: 'fr'
}; };
let Fields = [];
Fields.push.apply(Fields, document.querySelectorAll('textarea[ckeditor]'));
// enable for custom fields
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));
Fields.forEach(function(field) {
ClassicEditor
.create( field )
.then( editor => {
//console.log( 'CkEditor was initialized', editor );
})
.catch( error => {
console.error( error.stack );
})
;
});

View File

@@ -1,15 +0,0 @@
import ClassicEditor from "./editor_config";
const ckeditorFields: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll('textarea[ckeditor]');
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
ClassicEditor
.create( field )
.then( editor => {
//console.log( 'CkEditor was initialized', editor );
})
.catch( error => {
console.error( error.stack );
})
;
});
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));

View File

@@ -1,16 +0,0 @@
import {download_report} from "../../lib/download-report/download-report";
window.addEventListener("DOMContentLoaded", function(e) {
const export_generate_url = window.export_generate_url;
if (typeof export_generate_url === 'undefined') {
console.error('Alias not found!');
throw new Error('Alias not found!');
}
const query = window.location.search,
container = document.querySelector("#download_container")
;
download_report(export_generate_url + query.toString(), container);
});

View File

@@ -1,6 +1,6 @@
export interface DateTime { export interface DateTime {
datetime: string; datetime: string;
datetime8601: string datetime8601: string;
} }
export interface Civility { export interface Civility {
@@ -12,8 +12,8 @@ export interface Job {
id: number; id: number;
type: "user_job"; type: "user_job";
label: { label: {
"fr": string; // could have other key. How to do that in ts ? fr: string; // could have other key. How to do that in ts ?
} };
} }
export interface Center { export interface Center {
@@ -26,8 +26,8 @@ export interface Scope {
id: number; id: number;
type: "scope"; type: "scope";
name: { name: {
"fr": string fr: string;
} };
} }
export interface User { export interface User {
@@ -42,15 +42,26 @@ export interface User {
// todo: mainCenter; mainJob; etc.. // todo: mainCenter; mainJob; etc..
} }
export interface UserGroup {
type: "chill_main_user_group" | "user_group";
id: number;
label: TranslatableString;
backgroundColor: string;
foregroundColor: string;
excludeKey: string;
}
export type UserGroupOrUser = User | UserGroup;
export interface UserAssociatedInterface { export interface UserAssociatedInterface {
type: "user"; type: "user";
id: number; id: number;
}; }
export type TranslatableString = { export type TranslatableString = {
fr?: string; fr?: string;
nl?: string; nl?: string;
} };
export interface Postcode { export interface Postcode {
id: number; id: number;
@@ -62,7 +73,7 @@ export interface Postcode {
export type Point = { export type Point = {
type: "Point"; type: "Point";
coordinates: [lat: number, lon: number]; coordinates: [lat: number, lon: number];
} };
export interface Country { export interface Country {
id: number; id: number;
@@ -70,7 +81,7 @@ export interface Country {
code: string; code: string;
} }
export type AddressRefStatus = 'match'|'to_review'|'reviewed'; export type AddressRefStatus = "match" | "to_review" | "reviewed";
export interface Address { export interface Address {
type: "address"; type: "address";
@@ -98,7 +109,7 @@ export interface Address {
} }
export interface AddressWithPoint extends Address { export interface AddressWithPoint extends Address {
point: Point point: Point;
} }
export interface AddressReference { export interface AddressReference {
@@ -138,7 +149,7 @@ export interface Location {
createdBy: User | null; createdBy: User | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
email: string | null email: string | null;
name: string; name: string;
phonenumber1: string | null; phonenumber1: string | null;
phonenumber2: string | null; phonenumber2: string | null;

View File

@@ -139,7 +139,7 @@ const postprocess = (html: string): string => {
} }
const convertMarkdownToHtml = (markdown: string): string => { const convertMarkdownToHtml = (markdown: string): string => {
marked.use({'hooks': {postprocess, preprocess}, 'async': false}); marked.use({'hooks': {postprocess, preprocess}});
const rawHtml = marked(markdown) as string; const rawHtml = marked(markdown) as string;
return rawHtml; return rawHtml;
}; };

View File

@@ -1,13 +1,24 @@
<template> <template>
<span class="chill-entity entity-user"> <span class="chill-entity entity-user">
{{ user.label }} {{ user.label }}
<span class="user-job" v-if="user.user_job !== null">({{ user.user_job.label.fr }})</span> <span class="main-scope" v-if="user.main_scope !== null">({{ user.main_scope.name.fr }})</span> <span v-if="user.isAbsent" class="badge bg-danger rounded-pill" :title="Absent">A</span> <span class="user-job" v-if="user.user_job !== null"
</span> >({{ user.user_job.label.fr }})</span
>
<span class="main-scope" v-if="user.main_scope !== null"
>({{ user.main_scope.name.fr }})</span
>
<span
v-if="user.isAbsent"
class="badge bg-danger rounded-pill"
:title="Absent"
>A</span
>
</span>
</template> </template>
<script> <script>
export default { export default {
name: "UserRenderBoxBadge", name: "UserRenderBoxBadge",
props: ['user'], props: ["user"],
} };
</script> </script>

View File

@@ -1,306 +0,0 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Catalogue
{% endblock %}
{% block css %}
<style media="screen">
h2 { margin: 1.5em 0; }
div.flex-table ul, div.flex-bloc ul { padding-left: 1rem; }
div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }
div.flex-bloc div.item-bloc { flex-basis: 50%; }
</style>
{% endblock %}
{% block content %}
<div class="col-md-10">
<h1 class="display-4">{{ block('title') }}</h1>
<b>Voir aussi: </b>
<a href="{{ path('sass_assets_test1') }}">Test 1</a> |
<a href="{{ path('sass_assets_test2') }}">Test 2</a>
<h2>Flex-table et flex-bloc</h2>
<p>Base d'un placement flex alternatif à l'usage des tables.
Flex-table et flex-bloc utilisent la même structure html (seul la root class change).
Le placement est responsive.
La bordure utilise box-shadow pour simuler border-collapse (table).
</p>
<p>Une classe separator peut être appliquée sur item-row</p>
<xmp>
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
<div class="item-row separator">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
</div>
</div>
</xmp>
<h3>Flex-table</h3>
<p>On fixe manuellement la largeur de la première colonne :
<pre>div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }</pre>
</p>
<div class="flex-table debug">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row separator">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
<div class="item-row">
<div class="item-col">Title row3</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
</div>
<h3>Flex-bloc</h3>
<p>On fixe manuellement la largeur des blocs :
<pre>div.flex-bloc div.item-bloc { flex-basis: 50%; }</pre>
</p>
<div class="flex-bloc debug">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row separator">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
<div class="item-row">
<div class="item-col">Title row3</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</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">
<div class="wl-row">
<div class="wl-col title">Usagers concernés</div>
<div class="wl-col list">
<p class="wl-item"><a href="#">Gaston Bah</a></p>
<p class="wl-item"><a href="#">Alain Bah</a></p>
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
<p class="wl-item"><a href="#">Corentine Bah</a></p>
<p class="wl-item"><a href="#">Justin Bah</a></p>
<p class="wl-item"><a href="#">Michel Sardou</a></p>
<p class="wl-item"><a href="#">Carine Rousseau</a></p>
<p class="wl-item"><a href="#">Mohamed Martin</a></p>
</div>
</div>
<div class="wl-row">
<div class="wl-col title">Problématiques sociales</div>
<div class="wl-col list">
<p class="wl-item"><a href="#">Gaston Bah</a></p>
<p class="wl-item"><a href="#">Alain Bah</a></p>
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
</div>
</div>
</div>
<xmp>
<div class="wrap-list">
<div class="wl-row">
<div class="wl-col title">title</div>
<div class="wl-col list">
<p class="wl-item">item</p>
<p class="wl-item">item</p>
...
</div>
</div>
...
</div>
</xmp>
<h2>Wrap-header</h2>
<p>Réglage d'une zone de titre sur 2 lignes.</p>
<div class="wrap-header debug">
<div class="wh-row">
<div class="wh-col">
<span class="h3"><b>Title</b></span>
<span class="badge rounded-pill bg-danger">badge</span>
</div>
<div class="wh-col">
<span class="badge rounded-pill bg-primary">badge</span>
</div>
</div>
<div class="wh-row">
<div class="wh-col">from startdate to enddate</div>
<div class="wh-col">text</div>
</div>
</div>
<xmp>
<div class="wrap-header">
<div class="wh-row">
<div class="wh-col">line1 left</div>
<div class="wh-col">line1 right</div>
</div>
<div class="wh-row">
<div class="wh-col">line2 left</div>
<div class="wh-col">line2 right</div>
</div>
</div>
</xmp>
<h2>Float-button top</h2>
<p>Une zone de bouton flotte à droite d'un contenu. On peut voir en faisant varier la largeur que celui-ci vient s'adapter harmonieusement autour des boutons.</p>
<div class="float-button top debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
</div>
<xmp>
<div class="float-button top">
<div class="box">
<div class="action">
floating button
</div>
content ...
</div>
</div>
</xmp>
<h2>Float-button bottom</h2>
<p>Avec la même structure, on accroche la zone de bouton en bas, toujours à droite. Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">source</a>. </p>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
</div>
<xmp>
<div class="float-button bottom">
<div class="box">
<div class="action">
floating button
</div>
content ...
</div>
</div>
</xmp>
<h1>Buttons</h1>
<ul class="record_actions">
<li><a href="#" class="btn btn-submit">submit</a></li>
<li><a href="#" class="btn btn-save">save</a></li>
<li><a href="#" class="btn btn-create">create</a></li>
<li><a href="#" class="btn btn-new">new</a></li>
<li><a href="#" class="btn btn-duplicate">duplicate</a></li>
<li><a href="#" class="btn btn-not-duplicate">not-duplicate</a></li>
<li><a href="#" class="btn btn-reset">reset</a></li>
<li><a href="#" class="btn btn-delete">delete</a></li>
<li><a href="#" class="btn btn-danger">danger</a></li>
<li><a href="#" class="btn btn-remove">remove</a></li>
<li><a href="#" class="btn btn-unlink">unlink</a></li>
<li><a href="#" class="btn btn-action">action</a></li>
<li><a href="#" class="btn btn-edit">edit</a></li>
<li><a href="#" class="btn btn-update">update</a></li>
<li><a href="#" class="btn btn-show">show</a></li>
<li><a href="#" class="btn btn-view">view</a></li>
<li><a href="#" class="btn btn-misc">misc</a></li>
<li><a href="#" class="btn btn-cancel">cancel</a></li>
<li><a href="#" class="btn btn-choose">choose</a></li>
<li><a href="#" class="btn btn-notify">notify</a></li>
<li><a href="#" class="btn btn-tpchild">tpchild</a></li>
<li><a href="#" class="btn btn-chill-beige">my button</a></li>
</ul>
<h2>Variants of <pre>record_actions</pre></h2>
<h3><pre>small</pre></h3>
<ul class="record_actions small">
<li><a href="#" class="btn btn-create"></a></li>
</ul>
<h3><pre>inline</pre></h3>
<div>
This is inline and small
<ul class="record_actions small inline">
<li><a href="#" class="btn btn-create"></a></li>
</ul>
</div>
<xmp><a class="btn btn-submit">Text</a></xmp>
Toutes les classes btn-* de bootstrap sont fonctionnelles
</div>
{% endblock %}

View File

@@ -1,84 +0,0 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Tests - page 1
{% endblock %}
{% block css %}
<style media="screen">
</style>
{% endblock %}
{% block content %}
<div class="col-md-8">
<h1>CSS Tests - page 1 : float-button</h1>
<h2>1) avec des li</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<ul class="list-content fa-ul">
<li><i class="fa fa-li fa-file-text-o"></i>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus.</li>
<li><i class="fa fa-li fa-map-marker"></i>
<div class="chill-entity entity-address my-3" data-v-8b2170ec="">
<div class="address multiline" data-v-8b2170ec="">
<p class="street" data-v-8b2170ec="">97, chemin Franck Julien, </p>
<p class="postcode" data-v-8b2170ec="">1000 Bruxelles</p>
<p class="country" data-v-8b2170ec="">Belgique</p>
</div>
<div class="address-more" data-v-8b2170ec="">
<div data-v-8b2170ec="">
<span class="corridor" data-v-8b2170ec="">
<b data-v-8b2170ec="">Couloir</b>: 3
</span>
</div>
</div>
</div>
</li>
<li><i class="fa fa-li fa-mobile"></i><a href="tel: +33 8 27 17 12 19">+33 8 27 17 12 19</a></li>
<li><i class="fa fa-li fa-envelope-o"></i><a href="mailto: gusikowski.yesenia@hotmail.com">gusikowski.yesenia@hotmail.com</a></li>
</ul>
</div>
</div>
<h2>2) avec des p</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<p>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</p>
<p>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam. Curabitur sem eros, consectetur a auctor vel, facilisis sit amet sem.</p>
<p>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</p>
</div>
</div>
<h2>3) avec des div</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<div>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</div>
<div>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam.
<a href="#">Curabitur</a> sem eros, consectetur a auctor vel, facilisis sit amet sem.</div>
<div>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,78 +0,0 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Tests - page 2
{% endblock %}
{% block css %}
{% endblock %}
{% block content %}
<div class="col-md-10">
<h1>CSS Tests - page 2: grid layout</h1>
<h2>1) mgrid 1-2: avec grid-column et grid-row</h2>
<div class="mgrid debug">
<div class="area1">
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="area2">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
</div>
<h2>2) lgrid 3-4: avec grid-template-areas</h2>
<div class="lgrid debug">
<div class="area3">
<i>La zone qu'on crée avec les noms doit être rectangulaires. Actuellement, il n'existe pas de méthode pour créer une zone avec une forme de L (bien que la spécification indique qu'une prochaine version pourrait couvrir cette fonctionnalité).
[...] Si des zones ne sont pas rectangulaires, cela sera également considéré comme invalide.</i>
Voir sur MDN: <a target="_blank" href="https://developer.mozilla.org/fr/docs/Web/CSS/CSS_Grid_Layout/Grid_Template_Areas#occuper_plusieurs_cellules">Définir des zones sur une grille</a>
</div>
<div class="area4">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
</div>
<h2>3) cgrid 5-6-7-8: avec masonry</h2>
<p>Expérimental: dans FF <i>about:config</i>, il faut mettre <i>layout.css.grid-template-masonry-value.enabled = true</i></p>
<div class="cgrid debug">
<div class="item">
1 Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
2 Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
3 Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
4 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
5 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan.
</div>
<div class="item">
6 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim.
</div>
<div class="item">
7 Proin hendrerit arcu velit, eu ultrices dui interdum eget.
</div>
<div class="item">
8 Eu ultrices dui interdum eget.
</div>
</div>
</div>
{% endblock %}

View File

@@ -40,7 +40,7 @@
{{ 'by_user'|trans ~ ' ' }} {{ 'by_user'|trans ~ ' ' }}
{% endif %} {% endif %}
<span class="user"> <span class="user">
{{ user|chill_entity_render_box({'at_date': comment.date}) }} {{ user|chill_entity_render_box(options['user']) }}
</span> </span>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,10 +1,10 @@
<span class="chill-entity entity-user"> <span class="chill-entity entity-user">
{{- user.label }} {{- user.label }}
{%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %} {%- if opts['user_job'] and user.userJob(opts['at']) is not null %}
<span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span> <span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span>
{%- endif -%} {%- endif -%}
{%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %} {%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %}
<span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span> <span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span>
{%- endif -%} {%- endif -%}
{%- if opts['absence'] and user.isAbsent %} {%- if opts['absence'] and user.isAbsent %}
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span> <span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>

View File

@@ -22,13 +22,15 @@
{% block js %} {% block js %}
<script type="text/javascript"> <script type="text/javascript">
window.export_generate_url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}"; window.addEventListener("DOMContentLoaded", function(e) {
</script> var url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}",
{{ encore_entry_link_tags('page_download_exports') }} query = window.location.search,
{% endblock %} container = document.querySelector("#download_container")
;
{% block css %} chill.download_report(url+query, container);
{{ encore_entry_script_tags('page_download_exports') }} });
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -1,20 +0,0 @@
{% if menus|length > 0 %}
<li class="dropdown">
<a class="dropdown-toggle btn btn-sm btn-outline-primary"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa fa-flash"></i>
</a>
<div class="dropdown-menu">
{% for menu in menus %}
<a class="dropdown-item"
href="{{ menu.uri }}"
><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i>
{{ menu.label|trans }}
</a>
{% endfor %}
</div>
</li>
{% endif %}

View File

@@ -21,7 +21,7 @@
</span> </span>
{% if not c.notification.isSystem %} {% if not c.notification.isSystem %}
<span class="badge-user"> <span class="badge-user">
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }} {{ c.notification.sender|chill_entity_render_string }}
</span> </span>
{% else %} {% else %}
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span> <span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
@@ -53,7 +53,7 @@
{% endif %} {% endif %}
{% for a in c.notification.addressees %} {% for a in c.notification.addressees %}
<span class="badge-user"> <span class="badge-user">
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }} {{ a|chill_entity_render_string }}
</span> </span>
{% endfor %} {% endfor %}
{% for a in c.notification.addressesEmails %} {% for a in c.notification.addressesEmails %}

View File

@@ -1,7 +1,7 @@
{% extends "@ChillMain/layout.html.twig" %} {% extends "@ChillMain/layout.html.twig" %}
{% block title 'notification.show notification from %sender%'|trans( {% block title 'notification.show notification from %sender%'|trans(
{ '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) } { '%sender%': notification.sender|chill_entity_render_string }
) ~ ' ' ~ notification.title %} ) ~ ' ' ~ notification.title %}
{% block js %} {% block js %}

View File

@@ -31,14 +31,14 @@
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
{{ 'By'|trans }} {{ 'By'|trans }}
{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}, {{ step.previous.transitionBy|chill_entity_render_box }},
{{ step.previous.transitionAt|format_datetime('short', 'short') }} {{ step.previous.transitionAt|format_datetime('short', 'short') }}
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="row"> <div class="row">
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div> <div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div> <div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-4">{{ 'Le'|trans }}</div> <div class="col-sm-4">{{ 'Le'|trans }}</div>
@@ -58,15 +58,17 @@
{{ form_row(transition_form.transition) }} {{ form_row(transition_form.transition) }}
</div> </div>
{% if transition_form.freezeAfter is defined %}
{{ form_row(transition_form.freezeAfter) }}
{% endif %}
<div id="futureDests"> <div id="futureDests">
{{ form_row(transition_form.futureDestUsers) }} {{ form_row(transition_form.future_dest_users) }}
{{ form_errors(transition_form.futureDestUsers) }}
{{ form_row(transition_form.futureCcUsers) }} {{ form_row(transition_form.future_cc_users) }}
{{ form_errors(transition_form.futureCcUsers) }}
{{ form_row(transition_form.futureDestEmails) }} {{ form_row(transition_form.future_dest_emails) }}
{{ form_errors(transition_form.futureDestEmails) }} {{ form_errors(transition_form.future_dest_users) }}
</div> </div>
<p>{{ form_label(transition_form.comment) }}</p> <p>{{ form_label(transition_form.comment) }}</p>
@@ -108,8 +110,8 @@
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %} {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul> <ul>
{% for u in entity_workflow.currentStepChained.destUserByAccessKey %} {% for u in entity_workflow.currentStep.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li> <li>{{ u|chill_entity_render_box }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@@ -42,7 +42,7 @@
<div class="item-col" style="width: inherit;"> <div class="item-col" style="width: inherit;">
{% if step.transitionBy is not null %} {% if step.transitionBy is not null %}
<div> <div>
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }} {{ step.transitionBy|chill_entity_render_box }}
</div> </div>
{% endif %} {% endif %}
<div> <div>
@@ -76,7 +76,7 @@
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p> <p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
<ul> <ul>
{% for u in step.destUser %} {% for u in step.destUser %}
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> <li>{{ u|chill_entity_render_box }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@@ -85,7 +85,7 @@
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p> <p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul> <ul>
{% for u in step.ccUser %} {% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> <li>{{ u|chill_entity_render_box }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@@ -103,7 +103,7 @@
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul> <ul>
{% for u in step.destUserByAccessKey %} {% for u in step.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li> <li>{{ u|chill_entity_render_box }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View File

@@ -1,19 +0,0 @@
<h2>{{ 'workflow.signature_zone.title'|trans }}</h2>
<div class="flex-table justify-content-center">
<div class="item-bloc">
{% for s in signatures %}
<div class="item-row mb-2">
<div class="col-sm-6"><span>{{ s.signer|chill_entity_render_box }}</span></div>
<div class="col-sm-6">
<a class="btn btn-show" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}">{{ 'workflow.signature_zone.button_sign'|trans }}</a>
{% if s.state is same as('signed') %}
<p class="updatedBy">{{ s.stateDate }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -1,24 +0,0 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
{{ 'Signature'|trans }}
{% endblock %}
{% block content %}
<div class="col-10 workflow">
<h1 class="mb-5">{{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}</h1>
{% if metadata_form is not null %}
{{ form_start(metadata_form) }}
{{ form_row(metadata_form.documentType) }}
{{ form_row(metadata_form.documentNumber) }}
{{ form_row(metadata_form.expirationDate) }}
<ul class="record_actions">
<li>
{{ form_widget(metadata_form.submit, { 'attr' : { 'class' : 'btn btn-submit' }} ) }}
</li>
</ul>
{{ form_end(metadata_form) }}
{% endif %}
</div>
{% endblock %}

View File

@@ -57,9 +57,6 @@
</section> </section>
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section> <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
{% if signatures|length > 0 %}
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
{% endif %}
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{# <section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #} <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section> <section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>

View File

@@ -3,7 +3,7 @@
{% if step.previous is not null %} {% if step.previous is not null %}
<li> <li>
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b> <b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
</li> </li>
<li> <li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
@@ -12,19 +12,19 @@
<li> <li>
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
<b> <b>
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %} {% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
</b> </b>
</li> </li>
<li> <li>
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
<b> <b>
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %} {% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
</b> </b>
</li> </li>
{% else %} {% else %}
<li> <li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b> <b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
</li> </li>
<li> <li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span> <span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>

View File

@@ -69,6 +69,7 @@
</div> </div>
{% endif %} {% endif %}
{% block wrapping_content %}
{% block content %} {% block content %}
<div class="col-8 main_search"> <div class="col-8 main_search">
{% if app.user.isAbsent %} {% if app.user.isAbsent %}
@@ -99,6 +100,7 @@
{% include '@ChillMain/Homepage/index.html.twig' %} {% include '@ChillMain/Homepage/index.html.twig' %}
{% endblock %} {% endblock %}
{% endblock %}
</div> </div>
{% endblock %} {% endblock %}

Some files were not shown because too many files have changed in this diff Show More