mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-11 01:04:57 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
c48625d1cd | |||
1195b54a68 | |||
2a280b814f | |||
230c758255 | |||
eafda987ae | |||
7db8a371fc | |||
0d0649dd31 | |||
ac12b8cdcf | |||
9c1611d052 | |||
90e3043c3d | |||
af13bf9088 | |||
4aa65d69c7 | |||
9e33aec594 | |||
f88bc7e9f0 | |||
8e78c41549
|
|||
6e36771349
|
|||
7a82cae155
|
|||
dfab223391 | |||
539752485c | |||
d204df0316 | |||
|
82c02f442b | ||
f32a9dc7bc | |||
ea06a96f91 | |||
76433e2512 | |||
1fa464b87a |
6
.changes/unreleased/Fixed-20250909-173639.yaml
Normal file
6
.changes/unreleased/Fixed-20250909-173639.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: Fix display of 'duplicate' and 'merge' buttons in CRUD templates
|
||||||
|
time: 2025-09-09T17:36:39.960419639+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
6
.changes/unreleased/Fixed-20250910-180935.yaml
Normal file
6
.changes/unreleased/Fixed-20250910-180935.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: Fix saving notification preferences in user's profile
|
||||||
|
time: 2025-09-10T18:09:35.525979715+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
10
.changes/v4.2.0.md
Normal file
10
.changes/v4.2.0.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## v4.2.0 - 2025-09-02
|
||||||
|
### Feature
|
||||||
|
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
|
||||||
|
|
||||||
|
**Schema Change**: Add columns or tables
|
||||||
|
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
|
||||||
|
### Fixed
|
||||||
|
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
|
||||||
|
* Fix typo in 'uncheckAll' script for centers selection
|
||||||
|
* Fix incorrect parameter name in event details link
|
6
.changes/v4.2.1.md
Normal file
6
.changes/v4.2.1.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
## v4.2.1 - 2025-09-03
|
||||||
|
### Fixed
|
||||||
|
* Fix exports to work with DirectExportInterface
|
||||||
|
### DX
|
||||||
|
* Improve error message when a stored object cannot be written on local disk
|
||||||
|
|
10
.changes/v4.3.0.md
Normal file
10
.changes/v4.3.0.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## v4.3.0 - 2025-09-08
|
||||||
|
### Feature
|
||||||
|
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
||||||
|
* Add a command to generate a list of permissions
|
||||||
|
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
||||||
|
|
||||||
|
**Schema Change**: Add columns or tables
|
||||||
|
### Fixed
|
||||||
|
* fix date formatting in calendar range display
|
||||||
|
* Change route URL to avoid clash with person duplicate controller method
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ migrations/*
|
|||||||
templates/*
|
templates/*
|
||||||
translations/*
|
translations/*
|
||||||
|
|
||||||
|
# we allow developers to add customization on their installation, without commiting it
|
||||||
|
config/packages/dev/*
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
|
@@ -27,11 +27,11 @@ Chill is a comprehensive web application built as a set of Symfony bundles. It i
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
Note: This is a project which exists from a long time ago, and we found multiple structure inside each bundle. When having the choice, the developers should choose the new structure.
|
Note: This is a project that's existed for a long time, and throughout the years we've used multiple structures inside each bundle. When having the choice, the developers should choose the new structure.
|
||||||
|
|
||||||
The project follows a standard Symfony bundle structure:
|
The project follows a standard Symfony bundle structure:
|
||||||
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
- `/src/Bundle/`: Contains all the Chill bundles. The code is either at the root of the bundle directory, or within a `src/` directory (preferred). See psr4 mapping at the root's `composer.json`.
|
||||||
- each bundle come with his own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside to the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
- each bundle comes with its own tests, either in the `Tests` directory (when the code is directly within the bundle directory (for instance `src/Bundle/ChillMainBundle/Tests`, `src/Bundle/ChillPersonBundle/Tests`)), or inside the `tests` directory, alongside the `src/` sub-directory (example: `src/Bundle/ChillWopiBundle/tests`) (this is the preferred way).
|
||||||
- `/docs/`: Contains project documentation
|
- `/docs/`: Contains project documentation
|
||||||
|
|
||||||
Each bundle typically has the following structure:
|
Each bundle typically has the following structure:
|
||||||
@@ -46,13 +46,13 @@ Each bundle typically has the following structure:
|
|||||||
|
|
||||||
### A special word about TicketBundle
|
### A special word about TicketBundle
|
||||||
|
|
||||||
The ticket bundle is developed using a kind of "Command" pattern. The controller fill a "Command", and a "CommandHandler" handle this command. They are savec in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
The ticket bundle is developed using a kind of "Command" pattern. The controller fills a "Command," and a "CommandHandler" handles this command. They are saved in the `src/Bundle/ChillTicketBundle/src/Action` directory.
|
||||||
|
|
||||||
## Development Guidelines
|
## Development Guidelines
|
||||||
|
|
||||||
### Building and Configuration Instructions
|
### Building and Configuration Instructions
|
||||||
|
|
||||||
All the command should be run through the `symfony` command, which will configure the required variables.
|
All the commands should be run through the `symfony` command, which will configure the required variables.
|
||||||
|
|
||||||
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
For assets, we must ensure that we use node at version `^20.0.0`. This is done using `nvm use 20`.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Set Up the Database**:
|
6. **Set Up the Database**:
|
||||||
```bash
|
```bash
|
||||||
# Create the database
|
# Create the database
|
||||||
symfony console doctrine:database:create
|
symfony console doctrine:database:create
|
||||||
@@ -99,20 +99,20 @@ For assets, we must ensure that we use node at version `^20.0.0`. This is done u
|
|||||||
symfony console doctrine:fixtures:load
|
symfony console doctrine:fixtures:load
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Build Assets**:
|
7. **Build Assets**:
|
||||||
```bash
|
```bash
|
||||||
nvm use 20
|
nvm use 20
|
||||||
yarn run encore dev
|
yarn run encore dev
|
||||||
```
|
```
|
||||||
|
|
||||||
7. **Start the Development Server**:
|
8. **Start the Development Server**:
|
||||||
```bash
|
```bash
|
||||||
symfony server:start -d
|
symfony server:start -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Setup
|
#### Docker Setup
|
||||||
|
|
||||||
The project includes Docker configuration for easier development:
|
The project includes a Docker configuration for easier development:
|
||||||
|
|
||||||
1. **Start Docker Services**:
|
1. **Start Docker Services**:
|
||||||
```bash
|
```bash
|
||||||
@@ -153,9 +153,9 @@ Key configuration files:
|
|||||||
|
|
||||||
Each time a doctrine entity is created, we generate migration to adapt the database.
|
Each time a doctrine entity is created, we generate migration to adapt the database.
|
||||||
|
|
||||||
The migration are created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, do not forget to quote the `\` (`\` must become `\\` in your command).
|
The migration is created using the command `symfony console doctrine:migrations:diff --no-interaction --namespace <namespace>`, where the namespace is the relevant namespace for migration. As this is a bash script, remember to quote the `\` (`\` must become `\\` in your command).
|
||||||
|
|
||||||
Each bundle has his own namespace for migration (always ask me to confirm that command, with a list of updated / created entities so that I can confirm you that it is ok):
|
Each bundle has his own namespace for migration (always ask me to confirm that command with a list of updated / created entities so that I can confirm to you that it is ok):
|
||||||
|
|
||||||
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
- `Chill\Bundle\ActivityBundle` writes migrations to `Chill\Migrations\Activity`;
|
||||||
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
- `Chill\Bundle\BudgetBundle` writes migrations to `Chill\Migrations\Budget`;
|
||||||
@@ -183,7 +183,7 @@ Once created the, comment's classes should be removed and a description of the c
|
|||||||
|
|
||||||
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
When we need to use a DateTime or DateTimeImmutable that need to express "now", we prefer the usage of
|
||||||
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
|
||||||
where injection does not work when restoring an entity from database, but usually possible in services.
|
where injection does not work when restoring an entity from a database, but usually possible in services.
|
||||||
|
|
||||||
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
|
||||||
where we have full and easy control of the date.
|
where we have full and easy control of the date.
|
||||||
@@ -198,9 +198,9 @@ The project uses PHPUnit for testing. Each bundle has its own test suite, and th
|
|||||||
|
|
||||||
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
For creating mock, we prefer using prophecy (library phpspec/prophecy).
|
||||||
|
|
||||||
##### Useful helpers and tips that avoid create a mock
|
##### Useful helpers and tips that avoid creating a mock
|
||||||
|
|
||||||
Some notable implementations that are tests helper, and avoid to create a mock:
|
Some notable implementations that are test helpers and avoid creating a mock:
|
||||||
|
|
||||||
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
|
||||||
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
|
||||||
@@ -297,7 +297,7 @@ class TicketTest extends TestCase
|
|||||||
|
|
||||||
#### Test Database
|
#### Test Database
|
||||||
|
|
||||||
For tests that require a database, the project uses postgresql database filled by fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
For tests that require a database, the project uses a postgresql database filled with fixtures (usage of doctrine-fixtures). You can configure a different database for testing in the `.env.test` file.
|
||||||
|
|
||||||
### Code Quality Tools
|
### Code Quality Tools
|
||||||
|
|
||||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@@ -6,6 +6,35 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
## v4.3.0 - 2025-09-08
|
||||||
|
### Feature
|
||||||
|
* ([#409](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/409)) Add 45 and 60 min calendar ranges
|
||||||
|
* Add a command to generate a list of permissions
|
||||||
|
* ([#412](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/412)) Add an absence end date
|
||||||
|
|
||||||
|
**Schema Change**: Add columns or tables
|
||||||
|
### Fixed
|
||||||
|
* fix date formatting in calendar range display
|
||||||
|
* Change route URL to avoid clash with person duplicate controller method
|
||||||
|
|
||||||
|
## v4.2.1 - 2025-09-03
|
||||||
|
### Fixed
|
||||||
|
* Fix exports to work with DirectExportInterface
|
||||||
|
### DX
|
||||||
|
* Improve error message when a stored object cannot be written on local disk
|
||||||
|
|
||||||
|
|
||||||
|
## v4.2.0 - 2025-09-02
|
||||||
|
### Feature
|
||||||
|
* ([#64](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/64)) Add external identifier for a Person
|
||||||
|
|
||||||
|
**Schema Change**: Add columns or tables
|
||||||
|
* ([#330](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/330) Allow users to choose for which notifications they want to receive an email
|
||||||
|
### Fixed
|
||||||
|
* ([#422](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/422)) Fixed html layout of pages for recovering password
|
||||||
|
* Fix typo in 'uncheckAll' script for centers selection
|
||||||
|
* Fix incorrect parameter name in event details link
|
||||||
|
|
||||||
## v4.1.0 - 2025-08-26
|
## v4.1.0 - 2025-08-26
|
||||||
### Feature
|
### Feature
|
||||||
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
|
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
|
||||||
|
@@ -55,5 +55,6 @@
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -70,6 +70,8 @@
|
|||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
|
<option value="00:45:00">45 minutes</option>
|
||||||
|
<option value="00:60:00">60 minutes</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select
|
||||||
|
@@ -32,6 +32,8 @@
|
|||||||
<option value="00:10:00">10 minutes</option>
|
<option value="00:10:00">10 minutes</option>
|
||||||
<option value="00:15:00">15 minutes</option>
|
<option value="00:15:00">15 minutes</option>
|
||||||
<option value="00:30:00">30 minutes</option>
|
<option value="00:30:00">30 minutes</option>
|
||||||
|
<option value="00:45:00">45 minutes</option>
|
||||||
|
<option value="00:60:00">60 minutes</option>
|
||||||
</select>
|
</select>
|
||||||
<label class="input-group-text" for="slotMinTime">De</label>
|
<label class="input-group-text" for="slotMinTime">De</label>
|
||||||
<select
|
<select
|
||||||
@@ -102,7 +104,8 @@
|
|||||||
event.title
|
event.title
|
||||||
}}</b>
|
}}</b>
|
||||||
<b v-else-if="event.extendedProps.is === 'range'"
|
<b v-else-if="event.extendedProps.is === 'range'"
|
||||||
>{{ formatDate(event.startStr) }} -
|
>{{ formatDate(event.startStr, "time") }} -
|
||||||
|
{{ formatDate(event.endStr, "time") }}:
|
||||||
{{ event.extendedProps.locationName }}</b
|
{{ event.extendedProps.locationName }}</b
|
||||||
>
|
>
|
||||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
<b v-else-if="event.extendedProps.is === 'local'">{{
|
||||||
@@ -294,9 +297,26 @@ const nextWeeks = computed((): Weeks[] =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDate = (datetime: string) => {
|
const formatDate = (datetime: string, format: null | "time" = null) => {
|
||||||
console.log(typeof datetime);
|
const date = ISOToDate(datetime);
|
||||||
return ISOToDate(datetime);
|
if (!date) return "";
|
||||||
|
|
||||||
|
if (format === "time") {
|
||||||
|
return date.toLocaleTimeString("fr-FR", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// French date formatting
|
||||||
|
return date.toLocaleDateString("fr-FR", {
|
||||||
|
weekday: "short",
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseOptions = ref<CalendarOptions>({
|
const baseOptions = ref<CalendarOptions>({
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
<?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\CustomFieldsBundle\EntityRepository;
|
||||||
|
|
||||||
|
use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class CustomFieldsDefaultGroupRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, CustomFieldsDefaultGroup::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findOneByEntity(string $className): ?CustomFieldsDefaultGroup
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['entity' => $className]);
|
||||||
|
}
|
||||||
|
}
|
@@ -127,3 +127,7 @@ services:
|
|||||||
factory: ["@doctrine", getRepository]
|
factory: ["@doctrine", getRepository]
|
||||||
arguments:
|
arguments:
|
||||||
- "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option"
|
- "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option"
|
||||||
|
|
||||||
|
Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
@@ -18,6 +18,7 @@ use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|||||||
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
use Chill\DocStoreBundle\Service\Cryptography\KeyGenerator;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Symfony\Component\Filesystem\Path;
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
|
||||||
@@ -147,16 +148,11 @@ class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
public function writeContent(string $filename, string $encryptedContent): void
|
public function writeContent(string $filename, string $encryptedContent): void
|
||||||
{
|
{
|
||||||
$fullPath = $this->buildPath($filename);
|
$fullPath = $this->buildPath($filename);
|
||||||
$dir = Path::getDirectory($fullPath);
|
|
||||||
|
|
||||||
if (!$this->filesystem->exists($dir)) {
|
try {
|
||||||
$this->filesystem->mkdir($dir);
|
$this->filesystem->dumpFile($fullPath, $encryptedContent);
|
||||||
}
|
} catch (IOExceptionInterface $exception) {
|
||||||
|
throw StoredObjectManagerException::unableToStoreDocumentOnDisk($exception);
|
||||||
$result = file_put_contents($fullPath, $encryptedContent);
|
|
||||||
|
|
||||||
if (false === $result) {
|
|
||||||
throw StoredObjectManagerException::unableToStoreDocumentOnDisk();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
||||||
|
|
||||||
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
|
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
|
||||||
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
|
<a href="{{ path('chill_event__event_show', { 'id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
|
||||||
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
|
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
|
||||||
<i class="fa fa-fw fa-eye"></i>
|
<i class="fa fa-fw fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
|
{{ entity.notes|chill_print_or_message("Aucune note", 'blockquote') }}
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
|
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">
|
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_job_report_index', { 'person': entity.person.id }) }}">
|
||||||
|
@@ -46,6 +46,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
|
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||||
|
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
|
@@ -206,6 +206,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||||
|
|
||||||
{% block content_view_actions_after %}
|
{% block content_view_actions_after %}
|
||||||
<li>
|
<li>
|
||||||
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">
|
<a class="btn btn-misc" href="{{ chill_return_path_or('chill_crud_immersion_bilan', { 'id': entity.id, 'person_id': entity.person.id }) }}">
|
||||||
|
@@ -94,6 +94,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% endblock crud_content_view_details %}
|
{% endblock crud_content_view_details %}
|
||||||
|
{% block content_view_actions_duplicate_link %}{% endblock %}
|
||||||
|
|
||||||
{% block content_view_actions_back %}
|
{% block content_view_actions_back %}
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
|
@@ -0,0 +1,64 @@
|
|||||||
|
<?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\Action\User\UpdateProfile;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
|
||||||
|
final class UpdateProfileCommand
|
||||||
|
{
|
||||||
|
public array $notificationFlags = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[PhonenumberConstraint]
|
||||||
|
public ?PhoneNumber $phonenumber,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function create(User $user, NotificationFlagManager $flagManager): self
|
||||||
|
{
|
||||||
|
$updateProfileCommand = new self($user->getPhonenumber());
|
||||||
|
|
||||||
|
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
||||||
|
$updateProfileCommand->setNotificationFlag(
|
||||||
|
$provider->getFlag(),
|
||||||
|
User::NOTIF_FLAG_IMMEDIATE_EMAIL,
|
||||||
|
$user->isNotificationSendImmediately($provider->getFlag())
|
||||||
|
);
|
||||||
|
$updateProfileCommand->setNotificationFlag(
|
||||||
|
$provider->getFlag(),
|
||||||
|
User::NOTIF_FLAG_DAILY_DIGEST,
|
||||||
|
$user->isNotificationDailyDigest($provider->getFlag())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updateProfileCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User::NOTIF_FLAG_IMMEDIATE_EMAIL|User::NOTIF_FLAG_DAILY_DIGEST $kind
|
||||||
|
*/
|
||||||
|
private function setNotificationFlag(string $type, string $kind, bool $value): void
|
||||||
|
{
|
||||||
|
if (!array_key_exists($type, $this->notificationFlags)) {
|
||||||
|
$this->notificationFlags[$type] = ['immediate_email' => true, 'daily_digest' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$k = match ($kind) {
|
||||||
|
User::NOTIF_FLAG_IMMEDIATE_EMAIL => 'immediate_email',
|
||||||
|
User::NOTIF_FLAG_DAILY_DIGEST => 'daily_digest',
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->notificationFlags[$type][$k] = $value;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
<?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\Action\User\UpdateProfile;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
|
final readonly class UpdateProfileCommandHandler
|
||||||
|
{
|
||||||
|
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
||||||
|
{
|
||||||
|
$user->setPhonenumber($command->phonenumber);
|
||||||
|
|
||||||
|
foreach ($command->notificationFlags as $flag => $values) {
|
||||||
|
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
||||||
|
$user->setNotificationDailyDigest($flag, $values['daily_digest']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\Command;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
#[AsCommand(name: 'chill:main:dump-list-permissions', description: 'Print a markdown reference of permissions (roles) grouped by title with dependencies).')]
|
||||||
|
final class DumpListPermissionsCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private readonly RoleDumper $roleDumper)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$markdown = $this->roleDumper->dumpAsMarkdown();
|
||||||
|
$output->writeln($markdown);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
@@ -48,6 +48,7 @@ class AbsenceController extends AbstractController
|
|||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
$user->setAbsenceStart(null);
|
$user->setAbsenceStart(null);
|
||||||
|
$user->setAbsenceEnd(null);
|
||||||
$em = $this->managerRegistry->getManager();
|
$em = $this->managerRegistry->getManager();
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
|
@@ -345,7 +345,7 @@ class ExportController extends AbstractController
|
|||||||
* @param array $dataExport Raw data from export step
|
* @param array $dataExport Raw data from export step
|
||||||
* @param array $dataFormatter Raw data from formatter step
|
* @param array $dataFormatter Raw data from formatter step
|
||||||
*/
|
*/
|
||||||
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array
|
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, ?array $dataFormatter, ?SavedExport $savedExport): array
|
||||||
{
|
{
|
||||||
if ($this->filterStatsByCenters) {
|
if ($this->filterStatsByCenters) {
|
||||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
||||||
@@ -365,7 +365,7 @@ class ExportController extends AbstractController
|
|||||||
$formExport->submit($dataExport);
|
$formExport->submit($dataExport);
|
||||||
$dataExport = $formExport->getData();
|
$dataExport = $formExport->getData();
|
||||||
|
|
||||||
if (\count($dataFormatter) > 0) {
|
if (is_array($dataFormatter) && \count($dataFormatter) > 0) {
|
||||||
$formFormatter = $this->createCreateFormExport(
|
$formFormatter = $this->createCreateFormExport(
|
||||||
$alias,
|
$alias,
|
||||||
'generate_formatter',
|
'generate_formatter',
|
||||||
@@ -381,7 +381,7 @@ class ExportController extends AbstractController
|
|||||||
'export' => $dataExport['export']['export'] ?? [],
|
'export' => $dataExport['export']['export'] ?? [],
|
||||||
'filters' => $dataExport['export']['filters'] ?? [],
|
'filters' => $dataExport['export']['filters'] ?? [],
|
||||||
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
||||||
'pick_formatter' => $dataExport['export']['pick_formatter']['alias'],
|
'pick_formatter' => ($dataExport['export']['pick_formatter'] ?? [])['alias'] ?? '',
|
||||||
'formatter' => $dataFormatter['formatter'] ?? [],
|
'formatter' => $dataFormatter['formatter'] ?? [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -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\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Form\UserProfileType;
|
|
||||||
use Chill\MainBundle\Security\ChillSecurity;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
|
|
||||||
final class UserProfileController extends AbstractController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly TranslatorInterface $translator,
|
|
||||||
private readonly ChillSecurity $security,
|
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User profile that allows editing of phonenumber and visualization of certain data.
|
|
||||||
*/
|
|
||||||
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted('ROLE_USER')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
$editForm = $this->createForm(UserProfileType::class, $user);
|
|
||||||
|
|
||||||
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
|
|
||||||
|
|
||||||
$editForm->handleRequest($request);
|
|
||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
|
||||||
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
|
|
||||||
$user->setNotificationFlags($notificationFlagsData);
|
|
||||||
|
|
||||||
$em = $this->managerRegistry->getManager();
|
|
||||||
$em->flush();
|
|
||||||
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_main_user_profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('@ChillMain/User/profile.html.twig', [
|
|
||||||
'user' => $user,
|
|
||||||
'form' => $editForm->createView(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,75 @@
|
|||||||
|
<?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\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Form\UpdateProfileType;
|
||||||
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
|
use Chill\MainBundle\Security\ChillSecurity;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final readonly class UserUpdateProfileController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
private ChillSecurity $security,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private NotificationFlagManager $notificationFlagManager,
|
||||||
|
private FormFactoryInterface $formFactory,
|
||||||
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
private Environment $twig,
|
||||||
|
private UpdateProfileCommandHandler $updateProfileCommandHandler,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User profile that allows editing of phonenumber and visualization of certain data.
|
||||||
|
*/
|
||||||
|
#[Route(path: '/{_locale}/main/user/my-profile', name: 'chill_main_user_profile')]
|
||||||
|
public function __invoke(Request $request, Session $session)
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted('ROLE_USER')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
$command = UpdateProfileCommand::create($user, $this->notificationFlagManager);
|
||||||
|
$editForm = $this->formFactory->create(UpdateProfileType::class, $command);
|
||||||
|
|
||||||
|
$editForm->handleRequest($request);
|
||||||
|
|
||||||
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
|
$this->updateProfileCommandHandler->updateProfile($user, $command);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$session->getFlashBag()->add('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
||||||
|
|
||||||
|
return new RedirectResponse($this->urlGenerator->generate('chill_main_user_profile'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($this->twig->render('@ChillMain/User/profile.html.twig', [
|
||||||
|
'user' => $user,
|
||||||
|
'form' => $editForm->createView(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
@@ -24,6 +24,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.
|
* User.
|
||||||
@@ -45,6 +46,8 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||||
private ?\DateTimeImmutable $absenceStart = null;
|
private ?\DateTimeImmutable $absenceStart = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $absenceEnd = null;
|
||||||
/**
|
/**
|
||||||
* Array where SAML attributes's data are stored.
|
* Array where SAML attributes's data are stored.
|
||||||
*/
|
*/
|
||||||
@@ -157,6 +160,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->absenceStart;
|
return $this->absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAbsenceEnd(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->absenceEnd;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get attributes.
|
* Get attributes.
|
||||||
*
|
*
|
||||||
@@ -336,7 +344,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
|
|
||||||
public function isAbsent(): bool
|
public function isAbsent(): bool
|
||||||
{
|
{
|
||||||
return null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new \DateTimeImmutable('now');
|
$now = new \DateTimeImmutable('now');
|
||||||
|
$absenceStart = $this->getAbsenceStart();
|
||||||
|
$absenceEnd = $this->getAbsenceEnd();
|
||||||
|
|
||||||
|
return null !== $absenceStart
|
||||||
|
&& $absenceStart <= $now
|
||||||
|
&& (null === $absenceEnd || $now <= $absenceEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -410,6 +424,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
$this->absenceStart = $absenceStart;
|
$this->absenceStart = $absenceStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setAbsenceEnd(?\DateTimeImmutable $absenceEnd): void
|
||||||
|
{
|
||||||
|
$this->absenceEnd = $absenceEnd;
|
||||||
|
}
|
||||||
|
|
||||||
public function setAttributeByDomain(string $domain, string $key, $value): self
|
public function setAttributeByDomain(string $domain, string $key, $value): self
|
||||||
{
|
{
|
||||||
$this->attributes[$domain][$key] = $value;
|
$this->attributes[$domain][$key] = $value;
|
||||||
@@ -633,46 +652,82 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNotificationFlags(): array
|
private function getNotificationFlagData(string $flag): array
|
||||||
{
|
{
|
||||||
return $this->notificationFlags;
|
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotificationFlags(array $notificationFlags)
|
|
||||||
{
|
|
||||||
$this->notificationFlags = $notificationFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNotificationFlagData(string $flag): array
|
|
||||||
{
|
|
||||||
return $this->notificationFlags[$flag] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotificationFlagData(string $flag, array $data): void
|
|
||||||
{
|
|
||||||
$this->notificationFlags[$flag] = $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isNotificationSendImmediately(string $type): bool
|
public function isNotificationSendImmediately(string $type): bool
|
||||||
{
|
{
|
||||||
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
|
return $this->isNotificationForElement($type, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
public function setNotificationImmediately(string $type, bool $active): void
|
||||||
|
{
|
||||||
|
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_IMMEDIATE_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotificationDailyDigest(string $type, bool $active): void
|
||||||
|
{
|
||||||
|
$this->setNotificationFlagElement($type, $active, self::NOTIF_FLAG_DAILY_DIGEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param self::NOTIF_FLAG_IMMEDIATE_EMAIL|self::NOTIF_FLAG_DAILY_DIGEST $kind
|
||||||
|
*/
|
||||||
|
private function setNotificationFlagElement(string $type, bool $active, string $kind): void
|
||||||
|
{
|
||||||
|
$notificationFlags = [...$this->notificationFlags];
|
||||||
|
$changed = false;
|
||||||
|
|
||||||
|
if (!isset($notificationFlags[$type])) {
|
||||||
|
$notificationFlags[$type] = [self::NOTIF_FLAG_IMMEDIATE_EMAIL];
|
||||||
|
$changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if ($active) {
|
||||||
|
if (!in_array($kind, $notificationFlags[$type], true)) {
|
||||||
|
$notificationFlags[$type] = [...$notificationFlags[$type], $kind];
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (in_array($kind, $notificationFlags[$type], true)) {
|
||||||
|
$notificationFlags[$type] = array_values(
|
||||||
|
array_filter($notificationFlags[$type], static fn ($k) => $k !== $kind)
|
||||||
|
);
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed) {
|
||||||
|
$this->notificationFlags = [...$notificationFlags];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isNotificationForElement(string $type, string $kind): bool
|
||||||
|
{
|
||||||
|
return in_array($kind, $this->getNotificationFlagData($type), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isNotificationDailyDigest(string $type): bool
|
public function isNotificationDailyDigest(string $type): bool
|
||||||
{
|
{
|
||||||
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
|
return $this->isNotificationForElement($type, self::NOTIF_FLAG_DAILY_DIGEST);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLocale(): string
|
public function getLocale(): string
|
||||||
{
|
{
|
||||||
return 'fr';
|
return 'fr';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Assert\Callback]
|
||||||
|
public function validateAbsenceDates(ExecutionContextInterface $context): void
|
||||||
|
{
|
||||||
|
if (null !== $this->getAbsenceEnd() && null === $this->getAbsenceStart()) {
|
||||||
|
$context->buildViolation(
|
||||||
|
'user.absence_end_requires_start'
|
||||||
|
)
|
||||||
|
->atPath('absenceEnd')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
|||||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter?: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||||
*/
|
*/
|
||||||
class ExportConfigNormalizer
|
class ExportConfigNormalizer
|
||||||
{
|
{
|
||||||
@@ -72,10 +72,14 @@ class ExportConfigNormalizer
|
|||||||
}
|
}
|
||||||
$serialized['aggregators'] = $aggregatorsSerialized;
|
$serialized['aggregators'] = $aggregatorsSerialized;
|
||||||
|
|
||||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
if ($export instanceof ExportInterface) {
|
||||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||||
|
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||||
|
} elseif ($export instanceof DirectExportInterface) {
|
||||||
|
$serialized['formatter'] = ['form' => [], 'version' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
return $serialized;
|
return $serialized;
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,12 @@ class ExportConfigNormalizer
|
|||||||
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
||||||
{
|
{
|
||||||
$export = $this->exportManager->getExport($exportAlias);
|
$export = $this->exportManager->getExport($exportAlias);
|
||||||
$formater = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
|
||||||
|
if ($export instanceof ExportInterface) {
|
||||||
|
$formatter = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
||||||
|
} else {
|
||||||
|
$formatter = null;
|
||||||
|
}
|
||||||
|
|
||||||
$filtersConfig = [];
|
$filtersConfig = [];
|
||||||
foreach ($serializedData['filters'] as $alias => $filterData) {
|
foreach ($serializedData['filters'] as $alias => $filterData) {
|
||||||
@@ -117,8 +126,8 @@ class ExportConfigNormalizer
|
|||||||
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
||||||
'filters' => $filtersConfig,
|
'filters' => $filtersConfig,
|
||||||
'aggregators' => $aggregatorsConfig,
|
'aggregators' => $aggregatorsConfig,
|
||||||
'pick_formatter' => $serializedData['pick_formatter'],
|
'pick_formatter' => $serializedData['pick_formatter'] ?? '',
|
||||||
'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
'formatter' => $formatter?->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||||
'centers' => [
|
'centers' => [
|
||||||
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
||||||
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
||||||
|
@@ -23,9 +23,14 @@ class AbsenceType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('absenceStart', ChillDateType::class, [
|
->add('absenceStart', ChillDateType::class, [
|
||||||
'required' => true,
|
'required' => false,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
|
])
|
||||||
|
->add('absenceEnd', ChillDateType::class, [
|
||||||
|
'required' => false,
|
||||||
|
'input' => 'datetime_immutable',
|
||||||
|
'label' => 'absence.Absence end',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,75 +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\DataMapper;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Symfony\Component\Form\DataMapperInterface;
|
|
||||||
|
|
||||||
final readonly class NotificationFlagDataMapper implements DataMapperInterface
|
|
||||||
{
|
|
||||||
public function __construct(private array $notificationFlagProviders) {}
|
|
||||||
|
|
||||||
public function mapDataToForms($viewData, $forms): void
|
|
||||||
{
|
|
||||||
if (null === $viewData) {
|
|
||||||
$viewData = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$formsArray = iterator_to_array($forms);
|
|
||||||
|
|
||||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
|
||||||
$flag = $flagProvider->getFlag();
|
|
||||||
|
|
||||||
if (isset($formsArray[$flag])) {
|
|
||||||
$flagForm = $formsArray[$flag];
|
|
||||||
|
|
||||||
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|
|
||||||
|| !array_key_exists($flag, $viewData);
|
|
||||||
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
|
|
||||||
|
|
||||||
if ($flagForm->has('immediate_email')) {
|
|
||||||
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
|
|
||||||
}
|
|
||||||
if ($flagForm->has('daily_email')) {
|
|
||||||
$flagForm->get('daily_email')->setData($dailyEmailChecked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mapFormsToData($forms, &$viewData): void
|
|
||||||
{
|
|
||||||
$formsArray = iterator_to_array($forms);
|
|
||||||
$viewData = [];
|
|
||||||
|
|
||||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
|
||||||
$flag = $flagProvider->getFlag();
|
|
||||||
|
|
||||||
if (isset($formsArray[$flag])) {
|
|
||||||
$flagForm = $formsArray[$flag];
|
|
||||||
$viewData[$flag] = [];
|
|
||||||
|
|
||||||
if (true === $flagForm['immediate_email']->getData()) {
|
|
||||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === $flagForm['daily_email']->getData()) {
|
|
||||||
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([] === $viewData[$flag]) {
|
|
||||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,11 +11,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type;
|
namespace Chill\MainBundle\Form\Type;
|
||||||
|
|
||||||
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
|
|
||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
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\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -30,27 +28,24 @@ class NotificationFlagsType extends AbstractType
|
|||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
|
|
||||||
|
|
||||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||||
$flag = $flagProvider->getFlag();
|
$flag = $flagProvider->getFlag();
|
||||||
$builder->add($flag, FormType::class, [
|
$flagBuilder = $builder->create($flag, options: [
|
||||||
'label' => $flagProvider->getLabel(),
|
'label' => $flagProvider->getLabel(),
|
||||||
'required' => false,
|
'compound' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->get($flag)
|
$flagBuilder
|
||||||
->add('immediate_email', CheckboxType::class, [
|
->add('immediate_email', CheckboxType::class, [
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'mapped' => false,
|
|
||||||
])
|
])
|
||||||
->add('daily_email', CheckboxType::class, [
|
->add('daily_digest', CheckboxType::class, [
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'mapped' => false,
|
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
|
$builder->add($flagBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +53,7 @@ class NotificationFlagsType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => null,
|
'data_class' => null,
|
||||||
|
'compound' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,31 +11,29 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form;
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
class UserProfileType extends AbstractType
|
class UpdateProfileType extends AbstractType
|
||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('notificationFlags', NotificationFlagsType::class, [
|
->add('notificationFlags', NotificationFlagsType::class)
|
||||||
'label' => false,
|
|
||||||
'mapped' => false,
|
|
||||||
])
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => \Chill\MainBundle\Entity\User::class,
|
'data_class' => UpdateProfileCommand::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -105,6 +105,11 @@ class UserType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'input' => 'datetime_immutable',
|
'input' => 'datetime_immutable',
|
||||||
'label' => 'absence.Absence start',
|
'label' => 'absence.Absence start',
|
||||||
|
])
|
||||||
|
->add('absenceEnd', ChillDateType::class, [
|
||||||
|
'required' => false,
|
||||||
|
'input' => 'datetime_immutable',
|
||||||
|
'label' => 'absence.Absence end',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
// @phpstan-ignore-next-line
|
||||||
|
@@ -170,13 +170,14 @@ div.banner {
|
|||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
font-size: 50%;
|
font-size: 50%;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
&:before { content: '(n°'; }
|
|
||||||
&:after { content: ')'; }
|
&.same-size {
|
||||||
|
font-size: unset;
|
||||||
|
font-weight: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span.age {
|
span.age {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
&:before { content: '('; }
|
|
||||||
&:after { content: ')'; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,8 +37,13 @@ export const ISOToDate = (str: string | null): Date | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
// If the string already contains time info, use it directly
|
||||||
|
if (str.includes("T") || str.includes(" ")) {
|
||||||
|
return new Date(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, parse date only
|
||||||
|
const [year, month, day] = str.split("-").map((p) => parseInt(p));
|
||||||
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
return new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,20 +74,19 @@ export const ISOToDatetime = (str: string | null): Date | null => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const datetimeToISO = (date: Date): string => {
|
export const datetimeToISO = (date: Date): string => {
|
||||||
let cal, time, offset;
|
const cal = [
|
||||||
cal = [
|
|
||||||
date.getFullYear(),
|
date.getFullYear(),
|
||||||
(date.getMonth() + 1).toString().padStart(2, "0"),
|
(date.getMonth() + 1).toString().padStart(2, "0"),
|
||||||
date.getDate().toString().padStart(2, "0"),
|
date.getDate().toString().padStart(2, "0"),
|
||||||
].join("-");
|
].join("-");
|
||||||
|
|
||||||
time = [
|
const time = [
|
||||||
date.getHours().toString().padStart(2, "0"),
|
date.getHours().toString().padStart(2, "0"),
|
||||||
date.getMinutes().toString().padStart(2, "0"),
|
date.getMinutes().toString().padStart(2, "0"),
|
||||||
date.getSeconds().toString().padStart(2, "0"),
|
date.getSeconds().toString().padStart(2, "0"),
|
||||||
].join(":");
|
].join(":");
|
||||||
|
|
||||||
offset = [
|
const offset = [
|
||||||
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
date.getTimezoneOffset() <= 0 ? "+" : "-",
|
||||||
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
Math.abs(Math.floor(date.getTimezoneOffset() / 60))
|
||||||
.toString()
|
.toString()
|
||||||
|
@@ -44,8 +44,6 @@ section.chill-entity {
|
|||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
span.id-number {
|
span.id-number {
|
||||||
&:before { content: '(n°'; }
|
|
||||||
&:after { content: ')'; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.moreinfo {}
|
p.moreinfo {}
|
||||||
|
@@ -44,17 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content_view_actions_duplicate_link %}
|
{% endblock content_view_actions_duplicate_link %}
|
||||||
{% block content_view_actions_merge %}
|
{% block content_view_actions_merge %}{% endblock %}
|
||||||
<li>
|
|
||||||
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
|
|
||||||
{ 'thirdparty_id': entity.id }) }}"
|
|
||||||
title="{{ 'Merge'|trans }}"
|
|
||||||
class="btn btn-misc">
|
|
||||||
<i class="bi bi-chevron-contract"></i>
|
|
||||||
{{ 'Merge'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endblock %}
|
|
||||||
{% block content_view_actions_edit_link %}
|
{% block content_view_actions_edit_link %}
|
||||||
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
||||||
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
||||||
|
@@ -63,8 +63,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const uncheckAll = () => {
|
const uncheckAll = () => {
|
||||||
const allCenters = document.getElementsByName('centers[center][]');
|
const allCenters = document.getElementsByName('centers[centers][]');
|
||||||
|
|
||||||
allCenters.forEach(checkbox => checkbox.checked = false)
|
allCenters.forEach(checkbox => checkbox.checked = false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
<header>
|
||||||
|
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
|
||||||
|
<div class="container-xxl">
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<a class="navbar-brand" href="{{ path('chill_main_homepage') }}">
|
||||||
|
{{ include('@ChillMain/Layout/_header-logo.html.twig') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
@@ -8,36 +8,36 @@
|
|||||||
|
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<h2>{{ 'absence.My absence'|trans }}</h2>
|
<h2>{{ 'absence.My absence'|trans }}</h2>
|
||||||
|
<div>
|
||||||
|
{% if user.absenceStart is not null %}
|
||||||
|
<div class="alert alert-success flash_message">{{ 'absence.You are listed as absent, as of {date, date, short}'|trans({
|
||||||
|
date: user.absenceStart
|
||||||
|
}) }}
|
||||||
|
{% if user.absenceEnd is not null %}
|
||||||
|
{{ 'until %date%'|trans({'%date%': user.absenceEnd|format_date('short') }) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning flash_message">{{ 'absence.No absence listed'|trans }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_row(form.absenceStart) }}
|
||||||
|
{{ form_row(form.absenceEnd) }}
|
||||||
|
|
||||||
{% if user.absenceStart is not null %}
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<div>
|
<li>
|
||||||
<p>{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}</p>
|
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
<ul class="record_actions sticky-form-buttons">
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_main_user_absence_unset') }}"
|
<button class="btn btn-save" type="submit">
|
||||||
class="btn btn-delete">{{ 'absence.Unset absence'|trans }}</a>
|
{{ 'Save'|trans }}
|
||||||
</li>
|
</button>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
{% else %}
|
{{ form_end(form) }}
|
||||||
<div>
|
</div>
|
||||||
<p class="chill-no-data-statement">{{ 'absence.No absence listed'|trans }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ form_start(form) }}
|
|
||||||
{{ form_row(form.absenceStart) }}
|
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
|
||||||
<li>
|
|
||||||
<button class="btn btn-save" type="submit">
|
|
||||||
{{ 'Save'|trans }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{{ form_end(form) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -26,11 +26,12 @@
|
|||||||
|
|
||||||
{{ 'Welcome' | trans }}<br/>
|
{{ 'Welcome' | trans }}<br/>
|
||||||
|
|
||||||
<b>
|
{% if app.user %}
|
||||||
{{ app.user.username }}
|
<b>
|
||||||
{{ render(controller('Chill\\MainBundle\\Controller\\UIController::showNotificationUserCounterAction')) }}
|
{{ app.user.username }}
|
||||||
</b>
|
{{ render(controller('Chill\\MainBundle\\Controller\\UIController::showNotificationUserCounterAction')) }}
|
||||||
|
</b>
|
||||||
|
{% endif %}
|
||||||
{% if is_granted('IS_IMPERSONATOR') %}
|
{% if is_granted('IS_IMPERSONATOR') %}
|
||||||
<i class="fa fa-wrench fa-lg" title="Impersonate mode"></i>
|
<i class="fa fa-wrench fa-lg" title="Impersonate mode"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -16,29 +16,16 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>
|
|
||||||
{{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }}
|
|
||||||
</title>
|
|
||||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
|
||||||
{{ encore_entry_link_tags('chill') }}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="navigation container-fluid">
|
|
||||||
<div class="col-4 d-md-none parent">
|
|
||||||
<div class="col-10 col-md-12 offset-2 logo-container">
|
|
||||||
<a href="{{ path('chill_main_homepage') }}">
|
|
||||||
<img class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div id="content">
|
{% set header_logo_only = 1 %}
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
{% block title %}{{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }}{% endblock %}
|
||||||
</body>
|
|
||||||
</html>
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
{% block password_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block title %}{{ "New password set"|trans }}{% endblock %}
|
{% block title %}{{ "New password set"|trans }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block password_content %}
|
||||||
<div class="col-10 centered">
|
<div class="col-10 centered">
|
||||||
|
|
||||||
<h1>{{ "New password set"|trans }}</h1>
|
<h1>{{ "New password set"|trans }}</h1>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block password_content %}
|
||||||
<div class="col-10 centered">
|
<div class="col-10 centered">
|
||||||
|
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
|
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
{% block title %}{{"Recover password"|trans}}{% endblock %}
|
{% block title %}{{"Recover password"|trans}}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block password_content %}
|
||||||
<div class="col-10 centered">
|
<div class="col-10 centered">
|
||||||
|
|
||||||
<h1>{{ 'Recover password'|trans }}</h1>
|
<h1>{{ 'Recover password'|trans }}</h1>
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block title "Check your email"|trans %}
|
{% block title "Check your email"|trans %}
|
||||||
|
|
||||||
{% block content %}
|
{% block password_content %}
|
||||||
|
|
||||||
<div class="col-10 centered">
|
<div class="col-10 centered">
|
||||||
|
|
||||||
|
@@ -64,7 +64,7 @@
|
|||||||
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
{{ form_widget(flag.immediate_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{{ form_widget(flag.daily_email, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
{{ form_widget(flag.daily_digest, {'label_attr': { 'class': 'checkbox-inline checkbox-switch'}}) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -30,7 +30,11 @@
|
|||||||
{{ include('@ChillMain/Layout/_debug.html.twig') }}
|
{{ include('@ChillMain/Layout/_debug.html.twig') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ include('@ChillMain/Layout/_header.html.twig') }}
|
{% if header_logo_only is defined and header_logo_only == 1 %}
|
||||||
|
{{ include('@ChillMain/Layout/_header_logo_only.html.twig') }}
|
||||||
|
{% else %}
|
||||||
|
{{ include('@ChillMain/Layout/_header.html.twig') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block top_banner %}{#
|
{% block top_banner %}{#
|
||||||
To use if you want to add a banner below the header (ie the menu)
|
To use if you want to add a banner below the header (ie the menu)
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
|
||||||
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
|
||||||
<span class="ms-auto">
|
<span class="ms-auto">
|
||||||
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
|
<a class="btn btn-delete" title="Modifier" href="{{ path('chill_main_user_absence_unset') }}">{{ 'absence.Unset absence'|trans }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
86
src/Bundle/ChillMainBundle/Security/RoleDumper.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?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\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class RoleDumper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RoleProvider $roleProvider,
|
||||||
|
private RoleHierarchyInterface $roleHierarchy,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function dumpAsMarkdown(): string
|
||||||
|
{
|
||||||
|
$roles = $this->roleProvider->getRoles();
|
||||||
|
$rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes();
|
||||||
|
|
||||||
|
// Group roles by title
|
||||||
|
$groups = [];
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$title = $this->roleProvider->getRoleTitle($role);
|
||||||
|
$title ??= 'Other';
|
||||||
|
$groups[$title][] = $role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort groups by title
|
||||||
|
ksort($groups, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
foreach ($groups as $title => $roleList) {
|
||||||
|
// Sort roles by translated label for deterministic output
|
||||||
|
usort($roleList, function (string $a, string $b): int {
|
||||||
|
$ta = $this->translator->trans($a);
|
||||||
|
$tb = $this->translator->trans($b);
|
||||||
|
|
||||||
|
return strcasecmp($ta, $tb);
|
||||||
|
});
|
||||||
|
|
||||||
|
$translatedTitle = $this->translator->trans($title);
|
||||||
|
$lines[] = '## '.$translatedTitle;
|
||||||
|
|
||||||
|
foreach ($roleList as $role) {
|
||||||
|
// Translate primary role
|
||||||
|
$translatedRole = $this->translator->trans($role);
|
||||||
|
|
||||||
|
// Scope marker: (S) if needs scope, (~~S~~) if no scope required
|
||||||
|
$needsScope = !in_array($role, $rolesWithoutScopes, true);
|
||||||
|
$scopeMarker = $needsScope ? '(S)' : '(~~S~~)';
|
||||||
|
|
||||||
|
// Compute dependent roles from hierarchy (exclude itself)
|
||||||
|
$reachable = $this->roleHierarchy->getReachableRoleNames([$role]);
|
||||||
|
$dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role));
|
||||||
|
|
||||||
|
// Translate dependents and sort deterministically
|
||||||
|
$translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents);
|
||||||
|
sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
if (count($translatedDependents) > 0) {
|
||||||
|
$lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents));
|
||||||
|
} else {
|
||||||
|
$lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a blank line between groups
|
||||||
|
$lines[] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim possible trailing blank line
|
||||||
|
$markdown = rtrim(implode("\n", $lines));
|
||||||
|
|
||||||
|
return $markdown."\n"; // End with newline for POSIX friendliness
|
||||||
|
}
|
||||||
|
}
|
@@ -52,12 +52,8 @@ class RoleProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the title for each role.
|
* Get the title for each role.
|
||||||
*
|
|
||||||
* @param string $role
|
|
||||||
*
|
|
||||||
* @return string the title of the role
|
|
||||||
*/
|
*/
|
||||||
public function getRoleTitle($role)
|
public function getRoleTitle(string $role): ?string
|
||||||
{
|
{
|
||||||
$this->initializeRolesTitlesCache();
|
$this->initializeRolesTitlesCache();
|
||||||
|
|
||||||
@@ -73,7 +69,7 @@ class RoleProvider
|
|||||||
/**
|
/**
|
||||||
* initialize the array for caching role and titles.
|
* initialize the array for caching role and titles.
|
||||||
*/
|
*/
|
||||||
private function initializeRolesTitlesCache()
|
private function initializeRolesTitlesCache(): void
|
||||||
{
|
{
|
||||||
// break if already initialized
|
// break if already initialized
|
||||||
if (null !== $this->rolesTitlesCache) {
|
if (null !== $this->rolesTitlesCache) {
|
||||||
|
@@ -39,6 +39,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'label' => '',
|
'label' => '',
|
||||||
'email' => '',
|
'email' => '',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
|
'absenceStart' => null,
|
||||||
|
'absenceEnd' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
||||||
@@ -77,6 +79,11 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
['docgen:expects' => PhoneNumber::class, 'groups' => 'docgen:read']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$absenceDatesContext = array_merge(
|
||||||
|
$context,
|
||||||
|
['docgen:expects' => \DateTimeImmutable::class, 'groups' => 'docgen:read']
|
||||||
|
);
|
||||||
|
|
||||||
if (null === $object && 'docgen' === $format) {
|
if (null === $object && 'docgen' === $format) {
|
||||||
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
||||||
}
|
}
|
||||||
@@ -99,6 +106,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||||
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||||
'isAbsent' => $object->isAbsent(),
|
'isAbsent' => $object->isAbsent(),
|
||||||
|
'absenceStart' => $this->normalizer->normalize($object->getAbsenceStart(), $format, $absenceDatesContext),
|
||||||
|
'absenceEnd' => $this->normalizer->normalize($object->getAbsenceEnd(), $format, $absenceDatesContext),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ('docgen' === $format) {
|
if ('docgen' === $format) {
|
||||||
|
@@ -0,0 +1,85 @@
|
|||||||
|
<?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 Action\User\UpdateProfile;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class UpdateProfileCommandHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testUpdateProfileWithNullPhoneAndFlags(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
// Pre-set some flags to opposite values to check they are updated
|
||||||
|
$flag = 'tickets';
|
||||||
|
$user->setNotificationImmediately($flag, true);
|
||||||
|
$user->setNotificationDailyDigest($flag, true);
|
||||||
|
|
||||||
|
$command = new UpdateProfileCommand(null);
|
||||||
|
$command->notificationFlags = [
|
||||||
|
$flag => [
|
||||||
|
'immediate_email' => false,
|
||||||
|
'daily_digest' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
|
||||||
|
|
||||||
|
self::assertNull($user->getPhonenumber(), 'Phone should be set to null');
|
||||||
|
self::assertFalse($user->isNotificationSendImmediately($flag));
|
||||||
|
self::assertFalse($user->isNotificationDailyDigest($flag));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateProfileWithPhoneAndMultipleFlags(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
$phone = new PhoneNumber();
|
||||||
|
$phone->setCountryCode(33); // France
|
||||||
|
$phone->setNationalNumber(612345678);
|
||||||
|
|
||||||
|
$command = new UpdateProfileCommand($phone);
|
||||||
|
$command->notificationFlags = [
|
||||||
|
'reports' => [
|
||||||
|
'immediate_email' => true,
|
||||||
|
'daily_digest' => false,
|
||||||
|
],
|
||||||
|
'activities' => [
|
||||||
|
'immediate_email' => false,
|
||||||
|
'daily_digest' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
(new UpdateProfileCommandHandler())->updateProfile($user, $command);
|
||||||
|
|
||||||
|
// Phone assigned
|
||||||
|
self::assertInstanceOf(PhoneNumber::class, $user->getPhonenumber());
|
||||||
|
self::assertSame(33, $user->getPhonenumber()->getCountryCode());
|
||||||
|
self::assertSame('612345678', (string) $user->getPhonenumber()->getNationalNumber());
|
||||||
|
|
||||||
|
// Flags applied
|
||||||
|
self::assertTrue($user->isNotificationSendImmediately('reports'));
|
||||||
|
self::assertFalse($user->isNotificationDailyDigest('reports'));
|
||||||
|
|
||||||
|
self::assertFalse($user->isNotificationSendImmediately('activities'));
|
||||||
|
self::assertTrue($user->isNotificationDailyDigest('activities'));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,103 @@
|
|||||||
|
<?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 Action\User\UpdateProfile;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||||
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Translation\TranslatableMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class UpdateProfileCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCreateTransfersPhonenumberAndNotificationFlags(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
// set a phone number
|
||||||
|
$phone = new PhoneNumber();
|
||||||
|
$phone->setCountryCode(32); // Belgium
|
||||||
|
$phone->setNationalNumber(471234567);
|
||||||
|
$user->setPhonenumber($phone);
|
||||||
|
|
||||||
|
// configure notification flags on the user via helpers
|
||||||
|
$flagA = 'foo';
|
||||||
|
$flagB = 'bar';
|
||||||
|
|
||||||
|
// For tickets: immediate true, daily false
|
||||||
|
$user->setNotificationImmediately($flagA, true);
|
||||||
|
$user->setNotificationDailyDigest($flagA, false);
|
||||||
|
|
||||||
|
// For reports: immediate false, daily true
|
||||||
|
$user->setNotificationImmediately($flagB, false);
|
||||||
|
$user->setNotificationDailyDigest($flagB, true);
|
||||||
|
|
||||||
|
// a third flag not explicitly set to validate default behavior from User
|
||||||
|
$flagC = 'foobar'; // by default immediate-email is true, daily-digest is false per User::getNotificationFlagData
|
||||||
|
|
||||||
|
$manager = $this->createNotificationFlagManager([$flagA, $flagB, $flagC]);
|
||||||
|
|
||||||
|
$command = UpdateProfileCommand::create($user, $manager);
|
||||||
|
|
||||||
|
// phone number transferred
|
||||||
|
self::assertInstanceOf(PhoneNumber::class, $command->phonenumber);
|
||||||
|
self::assertSame($phone->getCountryCode(), $command->phonenumber->getCountryCode());
|
||||||
|
self::assertSame($phone->getNationalNumber(), $command->phonenumber->getNationalNumber());
|
||||||
|
|
||||||
|
// flags transferred consistently
|
||||||
|
self::assertArrayHasKey($flagA, $command->notificationFlags);
|
||||||
|
self::assertArrayHasKey($flagB, $command->notificationFlags);
|
||||||
|
self::assertArrayHasKey($flagC, $command->notificationFlags);
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'immediate_email' => true,
|
||||||
|
'daily_digest' => false,
|
||||||
|
], $command->notificationFlags[$flagA]);
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'immediate_email' => false,
|
||||||
|
'daily_digest' => true,
|
||||||
|
], $command->notificationFlags[$flagB]);
|
||||||
|
|
||||||
|
// default from User::getNotificationFlagData -> immediate true, daily false
|
||||||
|
self::assertSame([
|
||||||
|
'immediate_email' => true,
|
||||||
|
'daily_digest' => false,
|
||||||
|
], $command->notificationFlags[$flagC]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createNotificationFlagManager(array $flags): NotificationFlagManager
|
||||||
|
{
|
||||||
|
$providers = array_map(fn (string $flag) => new class ($flag) implements NotificationFlagProviderInterface {
|
||||||
|
public function __construct(private readonly string $flag) {}
|
||||||
|
|
||||||
|
public function getFlag(): string
|
||||||
|
{
|
||||||
|
return $this->flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): TranslatableMessage
|
||||||
|
{
|
||||||
|
return new TranslatableMessage($this->flag);
|
||||||
|
}
|
||||||
|
}, $flags);
|
||||||
|
|
||||||
|
return new NotificationFlagManager($providers);
|
||||||
|
}
|
||||||
|
}
|
@@ -96,11 +96,13 @@ final class NotificationTest extends KernelTestCase
|
|||||||
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
|
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when no notification flags are set, by default immediate email');
|
||||||
|
|
||||||
// immediate-email preference
|
// immediate-email preference
|
||||||
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL, User::NOTIF_FLAG_DAILY_DIGEST]]);
|
$user->setNotificationImmediately('test_notification_type', true);
|
||||||
|
$user->setNotificationDailyDigest('test_notification_type', true);
|
||||||
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
|
$this->assertTrue($user->isNotificationSendImmediately($notification->getType()), 'Should return true when preferences contain immediate-email');
|
||||||
|
|
||||||
// daily-email preference
|
// daily-email preference
|
||||||
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_DAILY_DIGEST]]);
|
$user->setNotificationDailyDigest('test_notification_type', true);
|
||||||
|
$user->setNotificationImmediately('test_notification_type', false);
|
||||||
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
|
$this->assertFalse($user->isNotificationSendImmediately($notification->getType()), 'Should return false when preference is daily-email only');
|
||||||
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
|
$this->assertTrue($user->isNotificationDailyDigest($notification->getType()), 'Should return true when preference is daily-email');
|
||||||
|
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
<?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\Tests\Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class UserNotificationFlagsPersistenceTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testFlushPersistsNotificationFlagsChanges(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$em = self::getContainer()->get('doctrine')->getManager();
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('user_'.bin2hex(random_bytes(4)));
|
||||||
|
$user->setLabel('Test User');
|
||||||
|
$user->setPassword('secret');
|
||||||
|
|
||||||
|
// Étape 1: créer et persister l’utilisateur
|
||||||
|
$em->persist($user);
|
||||||
|
$em->flush();
|
||||||
|
$id = $user->getId();
|
||||||
|
self::assertNotNull($id, 'User should have an ID after flush');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sanity check: par défaut, pas de daily digest pour "alerts"
|
||||||
|
self::assertFalse($user->isNotificationDailyDigest('alerts'));
|
||||||
|
|
||||||
|
// Étape 2: activer le daily digest -> setNotificationFlagElement réassigne la propriété
|
||||||
|
$user->setNotificationDailyDigest('alerts', true);
|
||||||
|
$em->flush(); // persist le changement
|
||||||
|
$em->clear(); // simule un nouveau cycle de requête
|
||||||
|
|
||||||
|
// Étape 3: recharger depuis la base et vérifier la persistance
|
||||||
|
/** @var User $reloaded */
|
||||||
|
$reloaded = $em->find(User::class, $id);
|
||||||
|
self::assertNotNull($reloaded);
|
||||||
|
self::assertTrue(
|
||||||
|
$reloaded->isNotificationDailyDigest('alerts'),
|
||||||
|
'Daily digest flag should be persisted'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Étape 4: modifier via setNotificationFlagData (remplacement du tableau)
|
||||||
|
// Cette méthode doit réassigner la propriété (copie -> réassignation)
|
||||||
|
$reloaded->setNotificationImmediately('alerts', true);
|
||||||
|
$reloaded->setNotificationDailyDigest('alerts', false);
|
||||||
|
$em->flush();
|
||||||
|
$em->clear();
|
||||||
|
|
||||||
|
/** @var User $reloaded2 */
|
||||||
|
$reloaded2 = $em->find(User::class, $id);
|
||||||
|
self::assertNotNull($reloaded2);
|
||||||
|
|
||||||
|
// Le daily digest n’est plus actif, seul immediate-email est présent
|
||||||
|
self::assertFalse($reloaded2->isNotificationDailyDigest('alerts'));
|
||||||
|
self::assertTrue($reloaded2->isNotificationSendImmediately('alerts'));
|
||||||
|
} finally {
|
||||||
|
// Nettoyage
|
||||||
|
$managed = $em->find(User::class, $id);
|
||||||
|
if (null !== $managed) {
|
||||||
|
$em->remove($managed);
|
||||||
|
$em->flush();
|
||||||
|
}
|
||||||
|
$em->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -67,4 +67,54 @@ class UserTest extends TestCase
|
|||||||
->first()->getEndDate()
|
->first()->getEndDate()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIsAbsent()
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
// Absent: today is within absence period
|
||||||
|
$absenceStart = new \DateTimeImmutable('-1 day');
|
||||||
|
$absenceEnd = new \DateTimeImmutable('+1 day');
|
||||||
|
$user->setAbsenceStart($absenceStart);
|
||||||
|
$user->setAbsenceEnd($absenceEnd);
|
||||||
|
self::assertTrue($user->isAbsent(), 'Should be absent when now is between start and end');
|
||||||
|
|
||||||
|
// Absent: end is null
|
||||||
|
$user->setAbsenceStart(new \DateTimeImmutable('-2 days'));
|
||||||
|
$user->setAbsenceEnd(null);
|
||||||
|
self::assertTrue($user->isAbsent(), 'Should be absent when started and no end');
|
||||||
|
|
||||||
|
// Not absent: absenceStart is in the future
|
||||||
|
$user->setAbsenceStart(new \DateTimeImmutable('+2 days'));
|
||||||
|
$user->setAbsenceEnd(null);
|
||||||
|
self::assertFalse($user->isAbsent(), 'Should not be absent if start is in the future');
|
||||||
|
|
||||||
|
// Not absent: absenceEnd is in the past
|
||||||
|
$user->setAbsenceStart(new \DateTimeImmutable('-5 days'));
|
||||||
|
$user->setAbsenceEnd(new \DateTimeImmutable('-1 day'));
|
||||||
|
self::assertFalse($user->isAbsent(), 'Should not be absent if end is in the past');
|
||||||
|
|
||||||
|
// Not absent: both are null
|
||||||
|
$user->setAbsenceStart(null);
|
||||||
|
$user->setAbsenceEnd(null);
|
||||||
|
self::assertFalse($user->isAbsent(), 'Should not be absent if start is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetNotification(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
|
||||||
|
self::assertTrue($user->isNotificationSendImmediately('dummy'));
|
||||||
|
self::assertFalse($user->isNotificationDailyDigest('dummy'));
|
||||||
|
|
||||||
|
$user->setNotificationImmediately('dummy', false);
|
||||||
|
self::assertFalse($user->isNotificationSendImmediately('dummy'));
|
||||||
|
|
||||||
|
$user->setNotificationDailyDigest('dummy', true);
|
||||||
|
self::assertTrue($user->isNotificationDailyDigest('dummy'));
|
||||||
|
|
||||||
|
$user->setNotificationImmediately('dummy', true);
|
||||||
|
self::assertTrue($user->isNotificationSendImmediately('dummy'));
|
||||||
|
self::assertTrue($user->isNotificationDailyDigest('dummy'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -144,7 +144,7 @@ class NotificationMailerTest extends TestCase
|
|||||||
$idProperty->setValue($user, 456);
|
$idProperty->setValue($user, 456);
|
||||||
|
|
||||||
// Set notification flags for the user
|
// Set notification flags for the user
|
||||||
$user->setNotificationFlags(['test_notification_type' => [User::NOTIF_FLAG_IMMEDIATE_EMAIL]]);
|
$user->setNotificationImmediately('test_notification_type', true);
|
||||||
|
|
||||||
$messageBus = $this->createMock(MessageBusInterface::class);
|
$messageBus = $this->createMock(MessageBusInterface::class);
|
||||||
$messageBus->expects($this->once())
|
$messageBus->expects($this->once())
|
||||||
|
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
98
src/Bundle/ChillMainBundle/Tests/Security/RoleDumperTest.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?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\Tests\Security;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||||
|
use Chill\MainBundle\Security\RoleDumper;
|
||||||
|
use Chill\MainBundle\Security\RoleProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RoleDumperTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDumpAsMarkdownGroupsByTitleTranslatesAndListsDependencies(): void
|
||||||
|
{
|
||||||
|
// Fake provider with two groups
|
||||||
|
$provider = new class () implements ProvideRoleHierarchyInterface {
|
||||||
|
public const R_PERSON_SEE = 'CHILL_PERSON_SEE';
|
||||||
|
public const R_PERSON_UPDATE = 'CHILL_PERSON_UPDATE';
|
||||||
|
public const R_REPORT_SEE = 'CHILL_REPORT_SEE';
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
return [self::R_PERSON_SEE, self::R_PERSON_UPDATE, self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithoutScope(): array
|
||||||
|
{
|
||||||
|
// In this test, assume REPORT_SEE does not need scope, others do
|
||||||
|
return [self::R_REPORT_SEE];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRolesWithHierarchy(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Person' => [self::R_PERSON_SEE, self::R_PERSON_UPDATE],
|
||||||
|
'Report' => [self::R_REPORT_SEE],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$roleProvider = new RoleProvider([$provider]);
|
||||||
|
|
||||||
|
// Fake role hierarchy: UPDATE implies SEE; others none
|
||||||
|
$roleHierarchy = new class () implements RoleHierarchyInterface {
|
||||||
|
public function getReachableRoleNames(array $roles): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
foreach ($roles as $r) {
|
||||||
|
$output[] = $r;
|
||||||
|
if ('CHILL_PERSON_UPDATE' === $r) {
|
||||||
|
$output[] = 'CHILL_PERSON_SEE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($output));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fake translator that clearly shows translation applied
|
||||||
|
$translator = new class () implements TranslatorInterface {
|
||||||
|
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return 'T('.$id.')';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$dumper = new RoleDumper($roleProvider, $roleHierarchy, $translator);
|
||||||
|
$md = $dumper->dumpAsMarkdown();
|
||||||
|
|
||||||
|
$expected = "## T(Person)\n"
|
||||||
|
."- **T(CHILL_PERSON_SEE)** (S)\n"
|
||||||
|
."- **T(CHILL_PERSON_UPDATE)** (S): T(CHILL_PERSON_SEE)\n\n"
|
||||||
|
."## T(Report)\n"
|
||||||
|
."- **T(CHILL_REPORT_SEE)** (~~S~~)\n";
|
||||||
|
|
||||||
|
self::assertSame($expected, $md);
|
||||||
|
}
|
||||||
|
}
|
@@ -101,6 +101,8 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => 'SomeUser',
|
'text_without_absent' => 'SomeUser',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
|
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
||||||
|
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
||||||
]];
|
]];
|
||||||
|
|
||||||
yield [$userNoPhone, 'docgen', ['docgen:expects' => User::class],
|
yield [$userNoPhone, 'docgen', ['docgen:expects' => User::class],
|
||||||
@@ -120,6 +122,8 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => 'AnotherUser',
|
'text_without_absent' => 'AnotherUser',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
|
'absenceStart' => ['context' => \DateTimeImmutable::class],
|
||||||
|
'absenceEnd' => ['context' => \DateTimeImmutable::class],
|
||||||
]];
|
]];
|
||||||
|
|
||||||
yield [null, 'docgen', ['docgen:expects' => User::class], [
|
yield [null, 'docgen', ['docgen:expects' => User::class], [
|
||||||
@@ -138,6 +142,8 @@ final class UserNormalizerTest extends TestCase
|
|||||||
'text_without_absent' => '',
|
'text_without_absent' => '',
|
||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
'main_center' => ['context' => Center::class],
|
'main_center' => ['context' => Center::class],
|
||||||
|
'absenceStart' => null,
|
||||||
|
'absenceEnd' => null,
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -113,3 +113,5 @@ services:
|
|||||||
Chill\MainBundle\Service\EntityInfo\ViewEntityInfoManager:
|
Chill\MainBundle\Service\EntityInfo\ViewEntityInfoManager:
|
||||||
arguments:
|
arguments:
|
||||||
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
|
$vienEntityInfoProviders: !tagged_iterator chill_main.entity_info_provider
|
||||||
|
|
||||||
|
Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommandHandler: ~
|
||||||
|
@@ -80,3 +80,7 @@ services:
|
|||||||
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand:
|
||||||
tags:
|
tags:
|
||||||
- {name: console.command}
|
- {name: console.command}
|
||||||
|
|
||||||
|
Chill\MainBundle\Command\DumpListPermissionsCommand:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250722140048 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add an absence end date for the user absence';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE users ADD absenceEnd TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN users.absenceEnd IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE users DROP absenceEnd');
|
||||||
|
}
|
||||||
|
}
|
@@ -136,3 +136,7 @@ filter_order:
|
|||||||
Search: Chercher dans la liste
|
Search: Chercher dans la liste
|
||||||
By date: Filtrer par date
|
By date: Filtrer par date
|
||||||
search_box: Filtrer par contenu
|
search_box: Filtrer par contenu
|
||||||
|
|
||||||
|
absence:
|
||||||
|
You are listed as absent, as of {date, date, short}: Votre absence est indiquée à partir du {date, date, short}
|
||||||
|
|
||||||
|
@@ -841,12 +841,12 @@ absence:
|
|||||||
# single letter for absence
|
# single letter for absence
|
||||||
A: A
|
A: A
|
||||||
My absence: Mon absence
|
My absence: Mon absence
|
||||||
Unset absence: Supprimer la date d'absence
|
Unset absence: Supprimer mes dates d'absence
|
||||||
Set absence date: Indiquer une date d'absence
|
Set absence date: Indiquer une date d'absence
|
||||||
Absence start: Absent à partir du
|
Absence start: Absent à partir du
|
||||||
|
Absence end: Jusqu'au
|
||||||
Absent: Absent
|
Absent: Absent
|
||||||
You are marked as being absent: Vous êtes indiqué absent.
|
You are marked as being absent: Vous êtes indiqué absent.
|
||||||
You are listed as absent, as of: Votre absence est indiquée à partir du
|
|
||||||
No absence listed: Aucune absence indiquée.
|
No absence listed: Aucune absence indiquée.
|
||||||
Is absent: Absent?
|
Is absent: Absent?
|
||||||
|
|
||||||
|
@@ -40,3 +40,7 @@ workflow:
|
|||||||
|
|
||||||
rolling_date:
|
rolling_date:
|
||||||
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
||||||
|
|
||||||
|
user:
|
||||||
|
absence_end_requires_start: "Vous ne pouvez pas renseigner une date de fin d'absence sans date de début."
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterfac
|
|||||||
use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface;
|
use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface;
|
||||||
use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass;
|
use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass;
|
||||||
use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface;
|
use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
|
use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface;
|
||||||
use Chill\PersonBundle\Widget\PersonListWidgetFactory;
|
use Chill\PersonBundle\Widget\PersonListWidgetFactory;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
@@ -38,5 +39,7 @@ class ChillPersonBundle extends Bundle
|
|||||||
->addTag('chill_person.list_person_customizer');
|
->addTag('chill_person.list_person_customizer');
|
||||||
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
|
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
|
||||||
->addTag('chill_main.notification_flag_provider');
|
->addTag('chill_main.notification_flag_provider');
|
||||||
|
$container->registerForAutoconfiguration(PersonIdentifierEngineInterface::class)
|
||||||
|
->addTag('chill_person.person_identifier_engine');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -78,7 +78,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
|||||||
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
|
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
|
||||||
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
#[Route(path: '/{_locale}/person/{acpw1_id}/acpw-duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
||||||
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
||||||
{
|
{
|
||||||
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
||||||
|
@@ -17,7 +17,6 @@ use Chill\PersonBundle\Entity\Household\Household;
|
|||||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Form\CreationPersonType;
|
use Chill\PersonBundle\Form\CreationPersonType;
|
||||||
use Chill\PersonBundle\Form\PersonType;
|
|
||||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||||
use Chill\PersonBundle\Repository\PersonRepository;
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
use Chill\PersonBundle\Search\SimilarPersonMatcher;
|
use Chill\PersonBundle\Search\SimilarPersonMatcher;
|
||||||
@@ -49,56 +48,6 @@ final class PersonController extends AbstractController
|
|||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/person/{person_id}/general/edit', name: 'chill_person_general_edit')]
|
|
||||||
public function editAction(int $person_id, Request $request)
|
|
||||||
{
|
|
||||||
$person = $this->_getPerson($person_id);
|
|
||||||
|
|
||||||
if (null === $person) {
|
|
||||||
throw $this->createNotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->denyAccessUnlessGranted(
|
|
||||||
'CHILL_PERSON_UPDATE',
|
|
||||||
$person,
|
|
||||||
'You are not allowed to edit this person'
|
|
||||||
);
|
|
||||||
|
|
||||||
$form = $this->createForm(
|
|
||||||
PersonType::class,
|
|
||||||
$person,
|
|
||||||
[
|
|
||||||
'cFGroup' => $this->getCFGroup(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && !$form->isValid()) {
|
|
||||||
$this->get('session')
|
|
||||||
->getFlashBag()->add('error', $this->translator
|
|
||||||
->trans('This form contains errors'));
|
|
||||||
} elseif ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
$this->get('session')->getFlashBag()
|
|
||||||
->add(
|
|
||||||
'success',
|
|
||||||
$this->translator
|
|
||||||
->trans('The person data has been updated')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_person_view', [
|
|
||||||
'person_id' => $person->getId(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(
|
|
||||||
'@ChillPerson/Person/edit.html.twig',
|
|
||||||
['person' => $person, 'form' => $form->createView()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCFGroup()
|
public function getCFGroup()
|
||||||
{
|
{
|
||||||
$cFGroup = null;
|
$cFGroup = null;
|
||||||
|
@@ -0,0 +1,79 @@
|
|||||||
|
<?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\PersonBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Form\PersonType;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||||
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Translation\TranslatableMessage;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final readonly class PersonEditController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private FormFactoryInterface $formFactory,
|
||||||
|
private CustomFieldsDefaultGroupRepository $customFieldsDefaultGroupRepository,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
private Environment $twig,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ParamConverter("person", options={"id": "person_id"})
|
||||||
|
*/
|
||||||
|
#[Route(path: '/{_locale}/person/{person_id}/general/edit', name: 'chill_person_general_edit')]
|
||||||
|
public function editAction(Person $person, Request $request, Session $session)
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(PersonVoter::UPDATE, $person)) {
|
||||||
|
throw new AccessDeniedHttpException('You are not allowed to edit this person.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->formFactory->create(
|
||||||
|
PersonType::class,
|
||||||
|
$person,
|
||||||
|
['cFGroup' => $this->customFieldsDefaultGroupRepository->findOneByEntity(Person::class)?->getCustomFieldsGroup()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && !$form->isValid()) {
|
||||||
|
$session
|
||||||
|
->getFlashBag()->add('error', new TranslatableMessage('This form contains errors'));
|
||||||
|
} elseif ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$session->getFlashBag()->add('success', new TranslatableMessage('The person data has been updated'));
|
||||||
|
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate('chill_person_view', ['person_id' => $person->getId()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($this->twig->render('@ChillPerson/Person/edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'person' => $person,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
@@ -110,6 +110,24 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end() // children for 'person_fields', parent = array 'person_fields'
|
->end() // children for 'person_fields', parent = array 'person_fields'
|
||||||
->end() // person_fields, parent = children of root
|
->end() // person_fields, parent = children of root
|
||||||
|
->arrayNode('person_render')
|
||||||
|
->addDefaultsIfNotSet()
|
||||||
|
->children()
|
||||||
|
->scalarNode('id_content_text')
|
||||||
|
->defaultValue('n°[[ person_id ]]')
|
||||||
|
->info(
|
||||||
|
<<<'EOF'
|
||||||
|
The way we display the person's id. Variables availables: "[[ person_id ]]", or, for person's
|
||||||
|
identifier: "[[ identifier_xx ]]" where xx is the identifier's definition's id.
|
||||||
|
|
||||||
|
There are also conditions available: "[[ if:identifier_yy ]] [[ identifier_yy ]] [[ endif:identifier_yy ]]"
|
||||||
|
|
||||||
|
Take care of keeping exactly one space between "[[" and the placeholder's content, and exactly one space before "]]"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
->end()
|
||||||
|
->end() // end of person_render's children
|
||||||
|
->end() // end of person_render
|
||||||
->arrayNode('household_fields')
|
->arrayNode('household_fields')
|
||||||
->canBeDisabled()
|
->canBeDisabled()
|
||||||
->children()
|
->children()
|
||||||
|
@@ -0,0 +1,83 @@
|
|||||||
|
<?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\PersonBundle\Entity\Identifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'chill_person_identifier')]
|
||||||
|
class PersonIdentifier
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Person::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'person_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private ?Person $person = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
|
private array $value = [];
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '[]'])]
|
||||||
|
private string $canonical = '';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||||
|
private PersonIdentifierDefinition $definition,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPerson(?Person $person): self
|
||||||
|
{
|
||||||
|
$this->person = $person;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPerson(): Person
|
||||||
|
{
|
||||||
|
return $this->person;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue(): array
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(array $value): void
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCanonical(): string
|
||||||
|
{
|
||||||
|
return $this->canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCanonical(string $canonical): void
|
||||||
|
{
|
||||||
|
$this->canonical = $canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefinition(): PersonIdentifierDefinition
|
||||||
|
{
|
||||||
|
return $this->definition;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,107 @@
|
|||||||
|
<?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\PersonBundle\Entity\Identifier;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'chill_person_identifier_definition')]
|
||||||
|
class PersonIdentifierDefinition
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'active', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
||||||
|
private bool $active = true;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[ORM\Column(name: 'label', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||||
|
private array $label,
|
||||||
|
#[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)]
|
||||||
|
private string $engine,
|
||||||
|
#[ORM\Column(name: 'is_searchable', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
|
||||||
|
private bool $isSearchable = false,
|
||||||
|
#[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])]
|
||||||
|
private bool $isEditableByUsers = false,
|
||||||
|
#[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
|
private array $data = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): array
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLabel(array $label): void
|
||||||
|
{
|
||||||
|
$this->label = $label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEngine(): string
|
||||||
|
{
|
||||||
|
return $this->engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEngine(string $engine): void
|
||||||
|
{
|
||||||
|
$this->engine = $engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSearchable(): bool
|
||||||
|
{
|
||||||
|
return $this->isSearchable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsSearchable(bool $isSearchable): void
|
||||||
|
{
|
||||||
|
$this->isSearchable = $isSearchable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEditableByUsers(): bool
|
||||||
|
{
|
||||||
|
return $this->isEditableByUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsEditableByUsers(bool $isEditableByUsers): void
|
||||||
|
{
|
||||||
|
$this->isEditableByUsers = $isEditableByUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive(): bool
|
||||||
|
{
|
||||||
|
return $this->active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActive(bool $active): self
|
||||||
|
{
|
||||||
|
$this->active = $active;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setData(array $data): void
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
}
|
@@ -31,6 +31,7 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|||||||
use Chill\PersonBundle\Entity\Household\Household;
|
use Chill\PersonBundle\Entity\Household\Household;
|
||||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
use Chill\PersonBundle\Entity\Person\PersonCenterCurrent;
|
use Chill\PersonBundle\Entity\Person\PersonCenterCurrent;
|
||||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||||
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
|
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
|
||||||
@@ -271,6 +272,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
private Collection $identifiers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The person's last name.
|
* The person's last name.
|
||||||
*/
|
*/
|
||||||
@@ -418,6 +422,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
$this->resources = new ArrayCollection();
|
$this->resources = new ArrayCollection();
|
||||||
$this->centerHistory = new ArrayCollection();
|
$this->centerHistory = new ArrayCollection();
|
||||||
$this->signatures = new ArrayCollection();
|
$this->signatures = new ArrayCollection();
|
||||||
|
$this->identifiers = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
@@ -498,6 +503,24 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addIdentifier(PersonIdentifier $identifier): self
|
||||||
|
{
|
||||||
|
if (!$this->identifiers->contains($identifier)) {
|
||||||
|
$this->identifiers[] = $identifier;
|
||||||
|
$identifier->setPerson($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeIdentifier(PersonIdentifier $identifier): self
|
||||||
|
{
|
||||||
|
$this->identifiers->removeElement($identifier);
|
||||||
|
$identifier->setPerson(null);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||||
{
|
{
|
||||||
$this->signatures->removeElement($signature);
|
$this->signatures->removeElement($signature);
|
||||||
@@ -1129,6 +1152,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ReadableCollection<int, PersonIdentifier>
|
||||||
|
*/
|
||||||
|
public function getIdentifiers(): ReadableCollection
|
||||||
|
{
|
||||||
|
return $this->identifiers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
@@ -1262,6 +1293,22 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this->spokenLanguages;
|
return $this->spokenLanguages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addSpokenLanguage(Language $language): self
|
||||||
|
{
|
||||||
|
if (!$this->spokenLanguages->contains($language)) {
|
||||||
|
$this->spokenLanguages->add($language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSpokenLanguage(Language $language): self
|
||||||
|
{
|
||||||
|
$this->spokenLanguages->removeElement($language);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): ?\DateTimeInterface
|
public function getUpdatedAt(): ?\DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
|
@@ -0,0 +1,73 @@
|
|||||||
|
<?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\PersonBundle\Form\DataMapper;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Exception\UnexpectedTypeException;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Symfony\Component\Form\DataMapperInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
|
||||||
|
final readonly class PersonIdentifiersDataMapper implements DataMapperInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PersonIdentifierManagerInterface $identifierManager,
|
||||||
|
private PersonIdentifierDefinitionRepository $identifierDefinitionRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function mapDataToForms($viewData, \Traversable $forms): void
|
||||||
|
{
|
||||||
|
if (!$viewData instanceof Collection) {
|
||||||
|
throw new UnexpectedTypeException($viewData, Collection::class);
|
||||||
|
}
|
||||||
|
/** @var array<string, FormInterface> $formsByKey */
|
||||||
|
$formsByKey = iterator_to_array($forms);
|
||||||
|
|
||||||
|
foreach ($this->identifierManager->getWorkers() as $worker) {
|
||||||
|
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$form = $formsByKey['identifier_'.$worker->getDefinition()->getId()];
|
||||||
|
$identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition()->getId() === $identifier->getId());
|
||||||
|
if (null === $identifier) {
|
||||||
|
$identifier = new PersonIdentifier($worker->getDefinition());
|
||||||
|
}
|
||||||
|
$form->setData($identifier->getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapFormsToData(\Traversable $forms, &$viewData): void
|
||||||
|
{
|
||||||
|
if (!$viewData instanceof Collection) {
|
||||||
|
throw new UnexpectedTypeException($viewData, Collection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($forms as $name => $form) {
|
||||||
|
$identifierId = (int) substr((string) $name, 11);
|
||||||
|
$identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getId() === $identifierId);
|
||||||
|
$definition = $this->identifierDefinitionRepository->find($identifierId);
|
||||||
|
if (null === $identifier) {
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$viewData->add($identifier);
|
||||||
|
}
|
||||||
|
if (!$identifier->getDefinition()->isEditableByUsers()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($definition);
|
||||||
|
$identifier->setValue($form->getData());
|
||||||
|
$identifier->setCanonical($worker->canonicalizeValue($identifier->getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
Normal file
48
src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?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\PersonBundle\Form;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final class PersonIdentifiersType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PersonIdentifierManagerInterface $identifierManager,
|
||||||
|
private readonly PersonIdentifiersDataMapper $identifiersDataMapper,
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
foreach ($this->identifierManager->getWorkers() as $worker) {
|
||||||
|
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subBuilder = $builder->create(
|
||||||
|
'identifier_'.$worker->getDefinition()->getId(),
|
||||||
|
options: [
|
||||||
|
'compound' => true,
|
||||||
|
'label' => $this->translatableStringHelper->localize($worker->getDefinition()->getLabel()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$worker->buildForm($subBuilder);
|
||||||
|
$builder->add($subBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->setDataMapper($this->identifiersDataMapper);
|
||||||
|
}
|
||||||
|
}
|
@@ -72,8 +72,8 @@ class PersonType extends AbstractType
|
|||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('firstName')
|
->add('firstName', TextType::class, ['empty_data' => ''])
|
||||||
->add('lastName')
|
->add('lastName', TextType::class, ['empty_data' => ''])
|
||||||
->add('birthdate', ChillDateType::class, [
|
->add('birthdate', ChillDateType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
@@ -101,7 +101,7 @@ class PersonType extends AbstractType
|
|||||||
|
|
||||||
if ('visible' === $this->config['memo']) {
|
if ('visible' === $this->config['memo']) {
|
||||||
$builder
|
$builder
|
||||||
->add('memo', ChillTextareaType::class, ['required' => false]);
|
->add('memo', ChillTextareaType::class, ['required' => false, 'empty_data' => '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('visible' === $this->config['employment_status']) {
|
if ('visible' === $this->config['employment_status']) {
|
||||||
@@ -118,6 +118,7 @@ class PersonType extends AbstractType
|
|||||||
$builder->add('placeOfBirth', TextType::class, [
|
$builder->add('placeOfBirth', TextType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'attr' => ['style' => 'text-transform: uppercase;'],
|
'attr' => ['style' => 'text-transform: uppercase;'],
|
||||||
|
'empty_data' => '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->get('placeOfBirth')->addModelTransformer(new CallbackTransformer(
|
$builder->get('placeOfBirth')->addModelTransformer(new CallbackTransformer(
|
||||||
@@ -127,7 +128,9 @@ class PersonType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ('visible' === $this->config['contact_info']) {
|
if ('visible' === $this->config['contact_info']) {
|
||||||
$builder->add('contactInfo', ChillTextareaType::class, ['required' => false]);
|
$builder->add('contactInfo', ChillTextareaType::class, [
|
||||||
|
'required' => false, 'empty_data' => '', 'label' => 'Notes on contact information',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('visible' === $this->config['phonenumber']) {
|
if ('visible' === $this->config['phonenumber']) {
|
||||||
@@ -152,12 +155,12 @@ class PersonType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
->add('acceptSMS', CheckboxType::class, [
|
->add('acceptSms', CheckboxType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder->add('otherPhoneNumbers', ChillCollectionType::class, [
|
$builder->add('otherPhonenumbers', ChillCollectionType::class, [
|
||||||
'entry_type' => PersonPhoneType::class,
|
'entry_type' => PersonPhoneType::class,
|
||||||
'button_add_label' => 'Add new phone',
|
'button_add_label' => 'Add new phone',
|
||||||
'button_remove_label' => 'Remove phone',
|
'button_remove_label' => 'Remove phone',
|
||||||
@@ -173,12 +176,12 @@ class PersonType extends AbstractType
|
|||||||
|
|
||||||
if ('visible' === $this->config['email']) {
|
if ('visible' === $this->config['email']) {
|
||||||
$builder
|
$builder
|
||||||
->add('email', EmailType::class, ['required' => false]);
|
->add('email', EmailType::class, ['required' => false, 'empty_data' => '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('visible' === $this->config['acceptEmail']) {
|
if ('visible' === $this->config['acceptEmail']) {
|
||||||
$builder
|
$builder
|
||||||
->add('acceptEmail', CheckboxType::class, ['required' => false]);
|
->add('acceptEmail', CheckboxType::class, ['required' => false, 'empty_data' => '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('visible' === $this->config['country_of_birth']) {
|
if ('visible' === $this->config['country_of_birth']) {
|
||||||
@@ -222,6 +225,10 @@ class PersonType extends AbstractType
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$builder->add('identifiers', PersonIdentifiersType::class, [
|
||||||
|
'by_reference' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
if ($options['cFGroup']) {
|
if ($options['cFGroup']) {
|
||||||
$builder
|
$builder
|
||||||
->add(
|
->add(
|
||||||
@@ -232,10 +239,7 @@ class PersonType extends AbstractType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
* @param OptionsResolverInterface $resolver
|
|
||||||
*/
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
|
||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => Person::class,
|
'data_class' => Person::class,
|
||||||
@@ -251,10 +255,7 @@ class PersonType extends AbstractType
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getBlockPrefix(): string
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getBlockPrefix()
|
|
||||||
{
|
{
|
||||||
return 'chill_personbundle_person';
|
return 'chill_personbundle_person';
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Exception;
|
||||||
|
|
||||||
|
class EngineNotFoundException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
parent::__construct("Engine for EngineInterface not found: {$name}");
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Exception;
|
||||||
|
|
||||||
|
class PersonIdentifierDefinitionNotFoundException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(int $id, ?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct("Person identifier definition not found by his id: {$id}", previous: $previous);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Exception;
|
||||||
|
|
||||||
|
class UnexpectedTypeException extends \InvalidArgumentException
|
||||||
|
{
|
||||||
|
public function __construct(mixed $value, string $expectedType)
|
||||||
|
{
|
||||||
|
parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value)));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Identifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
||||||
|
{
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'chill-person-bundle.string-identifier';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
return $value['content'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void
|
||||||
|
{
|
||||||
|
$builder->add('content', TextType::class, ['label' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
return $identifier?->getValue()['content'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
interface PersonIdentifierEngineInterface
|
||||||
|
{
|
||||||
|
public static function getName(): string;
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string;
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void;
|
||||||
|
|
||||||
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string;
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException;
|
||||||
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
||||||
|
|
||||||
|
final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private iterable $engines,
|
||||||
|
private PersonIdentifierDefinitionRepository $personIdentifierDefinitionRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build PersonIdentifierWorker's for all active definition.
|
||||||
|
*
|
||||||
|
* @return list<PersonIdentifierWorker>
|
||||||
|
*/
|
||||||
|
public function getWorkers(): array
|
||||||
|
{
|
||||||
|
$workers = [];
|
||||||
|
foreach ($this->personIdentifierDefinitionRepository->findByActive() as $definition) {
|
||||||
|
try {
|
||||||
|
$worker = $this->getEngine($definition->getEngine());
|
||||||
|
} catch (EngineNotFoundException) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workers[] = new PersonIdentifierWorker($worker, $definition);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $workers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
|
||||||
|
{
|
||||||
|
return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throw EngineNotFoundException
|
||||||
|
*/
|
||||||
|
private function getEngine(string $name): PersonIdentifierEngineInterface
|
||||||
|
{
|
||||||
|
foreach ($this->engines as $engine) {
|
||||||
|
if ($engine->getName() === $name) {
|
||||||
|
return $engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EngineNotFoundException($name);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
|
||||||
|
interface PersonIdentifierManagerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Build PersonIdentifierWorker's for all active definition.
|
||||||
|
*
|
||||||
|
* @return list<PersonIdentifierWorker>
|
||||||
|
*/
|
||||||
|
public function getWorkers(): array;
|
||||||
|
|
||||||
|
public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final readonly class PersonIdentifierWorker
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PersonIdentifierEngineInterface $identifierEngine,
|
||||||
|
private PersonIdentifierDefinition $definition,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getIdentifierEngine(): PersonIdentifierEngineInterface
|
||||||
|
{
|
||||||
|
return $this->identifierEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefinition(): PersonIdentifierDefinition
|
||||||
|
{
|
||||||
|
return $this->definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder): void
|
||||||
|
{
|
||||||
|
$this->identifierEngine->buildForm($builder, $this->definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value): ?string
|
||||||
|
{
|
||||||
|
return $this->identifierEngine->canonicalizeValue($value, $this->definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderAsString(?PersonIdentifier $identifier): string
|
||||||
|
{
|
||||||
|
return $this->identifierEngine->renderAsString($identifier, $this->definition);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Rendering;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
|
||||||
|
final readonly class PersonIdRendering implements PersonIdRenderingInterface
|
||||||
|
{
|
||||||
|
private string $idContentText;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ParameterBagInterface $parameterBag,
|
||||||
|
private PersonIdentifierManagerInterface $personIdentifierManager,
|
||||||
|
) {
|
||||||
|
$this->idContentText = $parameterBag->get('chill_person')['person_render']['id_content_text'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderPersonId(Person $person): string
|
||||||
|
{
|
||||||
|
$args = [
|
||||||
|
'[[ person_id ]]' => $person->getId(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($person->getIdentifiers() as $identifier) {
|
||||||
|
if (!$identifier->getDefinition()->isActive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = 'identifier_'.$identifier->getDefinition()->getId();
|
||||||
|
|
||||||
|
$args
|
||||||
|
+= [
|
||||||
|
"[[ {$key} ]]" => $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition())
|
||||||
|
->renderAsString($identifier),
|
||||||
|
"[[ if:{$key} ]]" => '',
|
||||||
|
"[[ endif:{$key} ]]" => '',
|
||||||
|
];
|
||||||
|
// we remove the eventual conditions
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$rendered = strtr($this->idContentText, $args);
|
||||||
|
|
||||||
|
// Delete the conditions which are not met, for instance:
|
||||||
|
// [[ if:identifier_99 ]] ... [[ endif:identifier_99 ]]
|
||||||
|
// this match the same dumber for opening and closing of the condition
|
||||||
|
return preg_replace(
|
||||||
|
'/\[\[\s*if:identifier_(\d+)\s*\]\].*?\[\[\s*endif:identifier_\1\s*\]\]/s',
|
||||||
|
'',
|
||||||
|
$rendered
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Rendering;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
|
||||||
|
interface PersonIdRenderingInterface
|
||||||
|
{
|
||||||
|
public function renderPersonId(Person $person): string;
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Rendering;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
final class PersonIdRenderingTwigExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PersonIdRenderingInterface $personIdRendering) {}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter(
|
||||||
|
'chill_person_id_render_text',
|
||||||
|
fn (Person $person): string => $this->personIdRendering->renderPersonId($person)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Rendering;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-implements ChillEntityRenderInterface<PersonIdentifier>
|
||||||
|
*/
|
||||||
|
final readonly class PersonIdentifierEntityRender implements ChillEntityRenderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private PersonIdentifierManagerInterface $identifierManager) {}
|
||||||
|
|
||||||
|
public function renderBox(mixed $entity, array $options): string
|
||||||
|
{
|
||||||
|
return $this->renderString($entity, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderString(mixed $entity, array $options): string
|
||||||
|
{
|
||||||
|
$worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($entity->getDefinition());
|
||||||
|
|
||||||
|
return $worker->renderAsString($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(object $entity, array $options): bool
|
||||||
|
{
|
||||||
|
return $entity instanceof PersonIdentifier;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Repository\Identifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-extends ServiceEntityRepository<PersonIdentifierDefinition>
|
||||||
|
*/
|
||||||
|
class PersonIdentifierDefinitionRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $managerRegistry)
|
||||||
|
{
|
||||||
|
parent::__construct($managerRegistry, PersonIdentifierDefinition::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByActive(): array
|
||||||
|
{
|
||||||
|
return $this->findBy(['active' => true]);
|
||||||
|
}
|
||||||
|
}
|
@@ -281,11 +281,6 @@ abbr.referrer { // still used ?
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.created-updated {
|
|
||||||
border: 1px solid black;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Masonry blocs on AccompanyingCourse resume page
|
/// Masonry blocs on AccompanyingCourse resume page
|
||||||
div#dashboards {
|
div#dashboards {
|
||||||
div.mbloc {
|
div.mbloc {
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<h1>
|
<h1>
|
||||||
<i class="fa fa-random fa-fw"></i>
|
<i class="fa fa-random fa-fw"></i>
|
||||||
{{ 'Accompanying Course'|trans }}
|
{{ 'Accompanying Course'|trans }}
|
||||||
<span class="id-number">{{ accompanyingCourse.id }}</span>
|
<span class="id-number">({{ 'accompanying_period.number'|trans({ 'id': accompanyingCourse.id}) }})</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -78,11 +78,6 @@
|
|||||||
{%- if options['addEntity'] -%}
|
{%- if options['addEntity'] -%}
|
||||||
<span class="badge rounded-pill bg-secondary">{{ 'Person'|trans }}</span>
|
<span class="badge rounded-pill bg-secondary">{{ 'Person'|trans }}</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if options['addId'] -%}
|
|
||||||
<span class="id-number" title="{{ 'Person'|trans ~ ' n° ' ~ person.id }}">
|
|
||||||
{{ person.id|upper -}}
|
|
||||||
</span>
|
|
||||||
{%- endif -%}
|
|
||||||
</div>
|
</div>
|
||||||
{%- if options['addInfo'] -%}
|
{%- if options['addInfo'] -%}
|
||||||
<p class="moreinfo">
|
<p class="moreinfo">
|
||||||
@@ -99,6 +94,12 @@
|
|||||||
{%- if options['addAge'] -%}
|
{%- if options['addAge'] -%}
|
||||||
<span class="age"> {{ 'years_old'|trans({ 'age': person.age }) }}</span>
|
<span class="age"> {{ 'years_old'|trans({ 'age': person.age }) }}</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
{%- if options['addId'] -%}
|
||||||
|
{%- set personId = person|chill_person_id_render_text %}
|
||||||
|
<span class="id-number" title="{{ 'Person'|trans ~ ' ' ~ personId }}">
|
||||||
|
({{ personId }})
|
||||||
|
</span>
|
||||||
|
{%- endif -%}
|
||||||
{%- elseif person.birthdate is not null -%}
|
{%- elseif person.birthdate is not null -%}
|
||||||
<time datetime="{{ person.birthdate|date('Y-m-d') }}" title="{{ 'Birthdate'|trans }}">
|
<time datetime="{{ person.birthdate|date('Y-m-d') }}" title="{{ 'Birthdate'|trans }}">
|
||||||
{{ 'Born the date'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral',
|
{{ 'Born the date'|trans({'gender': person.gender ? person.gender.genderTranslation.value : 'neutral',
|
||||||
@@ -108,6 +109,12 @@
|
|||||||
<span class="age">{{- 'years_old'|trans({ 'age': person.age }) -}}</span>
|
<span class="age">{{- 'years_old'|trans({ 'age': person.age }) -}}</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
{%- if options['addId'] -%}
|
||||||
|
{%- set personId = person|chill_person_id_render_text %}
|
||||||
|
<span class="id-number same-size" title="{{ 'Person'|trans ~ ' ' ~ personId }}">
|
||||||
|
({{ personId }})
|
||||||
|
</span>
|
||||||
|
{%- endif -%}
|
||||||
</p>
|
</p>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{#- tricks to remove easily whitespace after template -#}
|
{#- tricks to remove easily whitespace after template -#}
|
||||||
|
@@ -31,7 +31,7 @@
|
|||||||
{% if form.memo is defined %}
|
{% if form.memo is defined %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><h2>{{ 'Memo'|trans }}</h2></legend>
|
<legend><h2>{{ 'Memo'|trans }}</h2></legend>
|
||||||
{{ form_row(form.memo, {'label' : 'Memo'} ) }}
|
{{ form_widget(form.memo, {'label' : 'Memo'} ) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -85,15 +85,17 @@
|
|||||||
{{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }}
|
{{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }}
|
||||||
</div>
|
</div>
|
||||||
<div id="personAcceptSMS">
|
<div id="personAcceptSMS">
|
||||||
{{ form_row(form.acceptSMS, {'label' : 'Accept short text message ?'}) }}
|
{{ form_row(form.acceptSms, {'label' : 'Accept short text message ?'}) }}
|
||||||
</div>
|
</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if form.otherPhoneNumbers is defined -%}
|
{%- if form.otherPhonenumbers is defined -%}
|
||||||
{{ form_widget(form.otherPhoneNumbers) }}
|
{{ form_widget(form.otherPhonenumbers) }}
|
||||||
{{ form_errors(form.otherPhoneNumbers) }}
|
{{ form_errors(form.otherPhonenumbers) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if form.contactInfo is defined -%}
|
{%- if form.contactInfo is defined -%}
|
||||||
{{ form_row(form.contactInfo, {'label': 'Notes on contact information'}) }}
|
{{ form_label(form.contactInfo) }}
|
||||||
|
{{ form_widget(form.contactInfo) }}
|
||||||
|
{{ form_errors(form.contactInfo) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -134,6 +136,20 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{% if form.identifiers|length > 0 %}
|
||||||
|
<fieldset>
|
||||||
|
<legend><h2>{{ 'person.Identifiers'|trans }}</h2></legend>
|
||||||
|
<div>
|
||||||
|
{% for f in form.identifiers %}
|
||||||
|
{{ form_row(f) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{% else %}
|
||||||
|
{{ form_widget(form.identifiers) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
@@ -1,19 +1,3 @@
|
|||||||
{#
|
|
||||||
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#}
|
|
||||||
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||||
|
|
||||||
{% set activeRouteKey = 'chill_person_view' %}
|
{% set activeRouteKey = 'chill_person_view' %}
|
||||||
@@ -78,6 +62,16 @@ This view should receive those arguments:
|
|||||||
{% else %}
|
{% else %}
|
||||||
<dd>{{ 'gender.not defined'|trans }}</dd>
|
<dd>{{ 'gender.not defined'|trans }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if person.genderComment.comment is not empty %}
|
||||||
|
<dt>{{ 'Gender comment'|trans }} :</dt>
|
||||||
|
<dd>
|
||||||
|
<div class="chill-user-quote">
|
||||||
|
{{ person.genderComment.comment|chill_markdown_to_html }}
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,16 +120,6 @@ This view should receive those arguments:
|
|||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if person.genderComment.comment is not empty %}
|
|
||||||
<div class="col-12">
|
|
||||||
<figure class="person-details">
|
|
||||||
<h2 class="chill-beige">{{ 'Gender comment'|trans }} :</h2>
|
|
||||||
<div class="chill-user-quote">
|
|
||||||
{{ person.genderComment.comment|chill_markdown_to_html }}
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -241,17 +225,20 @@ This view should receive those arguments:
|
|||||||
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
|
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>{{ 'Comment on the marital status'|trans }} :</dt>
|
{% if person.maritalStatusComment.comment is not empty %}
|
||||||
|
<dt>{{ 'Comment on the marital status'|trans }} :</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if person.maritalStatusComment.comment is not empty %}
|
<blockquote class="chill-user-quote">
|
||||||
<blockquote class="chill-user-quote">
|
{{ person.maritalStatusComment.comment|chill_markdown_to_html }}
|
||||||
{{ person.maritalStatusComment.comment|chill_markdown_to_html }}
|
</blockquote>
|
||||||
</blockquote>
|
</dd>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
|
{% for identifier in person.identifiers %}
|
||||||
|
{% if identifier.definition.isActive and (identifier|chill_entity_render_string) is not empty %}
|
||||||
|
<dt>{{ identifier.definition.label|localize_translatable_string }} :</dt>
|
||||||
|
<dd>{{ identifier|chill_entity_render_box }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</figure>
|
</figure>
|
||||||
@@ -341,7 +328,7 @@ This view should receive those arguments:
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="created-updated">
|
<div>
|
||||||
{% if person.createdBy %}
|
{% if person.createdBy %}
|
||||||
<div class="createdBy">
|
<div class="createdBy">
|
||||||
{{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box({'at_date': person.createdAt}) }}</b>,<br>
|
{{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box({'at_date': person.createdAt}) }}</b>,<br>
|
||||||
|
@@ -23,7 +23,11 @@ class PersonRender implements PersonRenderInterface
|
|||||||
{
|
{
|
||||||
use BoxUtilsChillEntityRenderTrait;
|
use BoxUtilsChillEntityRenderTrait;
|
||||||
|
|
||||||
public function __construct(private readonly ConfigPersonAltNamesHelper $configAltNamesHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
|
public function __construct(
|
||||||
|
private readonly ConfigPersonAltNamesHelper $configAltNamesHelper,
|
||||||
|
private readonly \Twig\Environment $engine,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function renderBox($person, array $options): string
|
public function renderBox($person, array $options): string
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,145 @@
|
|||||||
|
<?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\PersonBundle\Tests\PersonIdentifier\Rendering;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Identifier\StringIdentifier;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRendering;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PersonIdRenderingTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideRenderCases
|
||||||
|
*/
|
||||||
|
public function testRenderPersonId(Person $person, string $idContentText, string $expected): void
|
||||||
|
{
|
||||||
|
// Parameter bag mock returning the provided id_content_text
|
||||||
|
$parameterBag = $this->prophesize(ParameterBagInterface::class);
|
||||||
|
$parameterBag->get('chill_person')
|
||||||
|
->willReturn(['person_render' => ['id_content_text' => $idContentText]]);
|
||||||
|
|
||||||
|
// PersonIdentifierManager is explicitly requested to be mocked in the spec.
|
||||||
|
// It will return a PersonIdentifierWorker whose renderAsString behaves like StringIdentifier::renderAsString
|
||||||
|
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
|
||||||
|
$personIdentifierManager
|
||||||
|
->buildWorkerByPersonIdentifierDefinition(Argument::type(PersonIdentifierDefinition::class))
|
||||||
|
->will(function ($args) {
|
||||||
|
/** @var PersonIdentifierDefinition $definition */
|
||||||
|
$definition = $args[0];
|
||||||
|
|
||||||
|
$engine = new class () implements PersonIdentifierEngineInterface {
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'test';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
return $value['content'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
|
||||||
|
|
||||||
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
// same behavior as StringIdentifier::renderAsString
|
||||||
|
return $identifier?->getValue()['content'] ?? '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new PersonIdentifierWorker($engine, $definition);
|
||||||
|
});
|
||||||
|
|
||||||
|
$service = new PersonIdRendering($parameterBag->reveal(), $personIdentifierManager->reveal());
|
||||||
|
|
||||||
|
self::assertSame($expected, $service->renderPersonId($person));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRenderCases(): iterable
|
||||||
|
{
|
||||||
|
// Case 1: one active identifier, one inactive identifier, should render person id and only active identifier
|
||||||
|
$person1 = new Person();
|
||||||
|
$this->setEntityId($person1, 123);
|
||||||
|
|
||||||
|
$defActive = new PersonIdentifierDefinition(label: ['en' => 'Active'], engine: 'string');
|
||||||
|
$this->setEntityId($defActive, 10);
|
||||||
|
$defActive->setActive(true);
|
||||||
|
|
||||||
|
$idActive = new PersonIdentifier($defActive);
|
||||||
|
$idActive->setPerson($person1);
|
||||||
|
$idActive->setValue(['content' => 'ABC']);
|
||||||
|
$person1->addIdentifier($idActive);
|
||||||
|
|
||||||
|
$defInactive = new PersonIdentifierDefinition(label: ['en' => 'Inactive'], engine: 'string');
|
||||||
|
$this->setEntityId($defInactive, 99);
|
||||||
|
$defInactive->setActive(false);
|
||||||
|
|
||||||
|
$idInactive = new PersonIdentifier($defInactive);
|
||||||
|
$idInactive->setPerson($person1);
|
||||||
|
$idInactive->setValue(['content' => 'SHOULD_NOT_APPEAR']);
|
||||||
|
$person1->addIdentifier($idInactive);
|
||||||
|
|
||||||
|
$template1 = 'ID: [[ person_id ]] - Active: [[ identifier_10 ]] - Inactive: [[ identifier_99 ]]';
|
||||||
|
$expected1 = 'ID: 123 - Active: ABC - Inactive: [[ identifier_99 ]]';
|
||||||
|
|
||||||
|
yield
|
||||||
|
'with active and inactive identifiers' => [$person1, $template1, $expected1]
|
||||||
|
;
|
||||||
|
|
||||||
|
$template2 = 'ID: [[ person_id ]][[ if:identifier_10 ]] - Active: [[ identifier_10 ]][[ endif:identifier_10 ]]';
|
||||||
|
$expected2 = 'ID: 123 - Active: ABC';
|
||||||
|
|
||||||
|
yield
|
||||||
|
'rendering with conditional: condition are removed' => [$person1, $template2, $expected2]
|
||||||
|
;
|
||||||
|
|
||||||
|
$template3 = 'ID: [[ person_id ]][[ if:identifier_99 ]] - Inactive: [[ identifier_10 ]][[ endif:identifier_99 ]]';
|
||||||
|
$expected3 = 'ID: 123';
|
||||||
|
|
||||||
|
yield
|
||||||
|
'rendering with conditional: the content between condition is removed' => [$person1, $template3, $expected3]
|
||||||
|
;
|
||||||
|
|
||||||
|
$template4 = 'ID: [[ person_id ]][[ if:identifier_105 ]] - not present: [[ identifier_105 ]][[ endif:identifier_105 ]]';
|
||||||
|
$expected4 = 'ID: 123';
|
||||||
|
|
||||||
|
yield
|
||||||
|
'rendering with conditional: the content between condition is removed, the identifier is not associated with the person' => [$person1, $template4, $expected4]
|
||||||
|
;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setEntityId(object $entity, int $id): void
|
||||||
|
{
|
||||||
|
$refl = new \ReflectionClass($entity);
|
||||||
|
$prop = $refl->getProperty('id');
|
||||||
|
$prop->setAccessible(true);
|
||||||
|
$prop->setValue($entity, $id);
|
||||||
|
}
|
||||||
|
}
|
@@ -95,3 +95,16 @@ services:
|
|||||||
Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodViewEntityInfoProvider:
|
Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodViewEntityInfoProvider:
|
||||||
arguments:
|
arguments:
|
||||||
$unions: !tagged_iterator chill_person.accompanying_period_info_part
|
$unions: !tagged_iterator chill_person.accompanying_period_info_part
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager:
|
||||||
|
arguments:
|
||||||
|
$engines: !tagged_iterator chill_person.person_identifier_engine
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface:
|
||||||
|
alias: Chill\PersonBundle\PersonIdentifier\PersonIdentifierManager
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\Identifier\:
|
||||||
|
resource: '../PersonIdentifier/Identifier'
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\Rendering\:
|
||||||
|
resource: '../PersonIdentifier/Rendering'
|
||||||
|
@@ -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\Migrations\Person;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250822123819 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add person identifier tables: chill_person_identifier_definition and chill_person_identifier with FKs to person and definition; create supporting sequences and indexes.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_person_identifier_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_person_identifier_definition_id_seq INCREMENT BY 1 MINVALUE 1 START 1000');
|
||||||
|
$this->addSql(
|
||||||
|
<<<'SQL'
|
||||||
|
CREATE TABLE chill_person_identifier (
|
||||||
|
id INT NOT NULL,
|
||||||
|
person_id INT NOT NULL,
|
||||||
|
definition_id INT NOT NULL,
|
||||||
|
value JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
canonical TEXT NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('CREATE INDEX IDX_BCA5A36B217BBB47 ON chill_person_identifier (person_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_BCA5A36BD11EA911 ON chill_person_identifier (definition_id)');
|
||||||
|
$this->addSql(
|
||||||
|
<<<'SQL'
|
||||||
|
CREATE TABLE chill_person_identifier_definition (
|
||||||
|
id INT NOT NULL,
|
||||||
|
label JSON DEFAULT '[]' NOT NULL,
|
||||||
|
engine VARCHAR(100) NOT NULL,
|
||||||
|
is_searchable BOOLEAN DEFAULT false NOT NULL,
|
||||||
|
is_editable_by_users BOOLEAN DEFAULT false NOT NULL,
|
||||||
|
data JSONB DEFAULT '[]' NOT NULL,
|
||||||
|
active BOOLEAN DEFAULT true NOT NULL,
|
||||||
|
PRIMARY KEY(id))
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier ADD CONSTRAINT FK_BCA5A36B217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier ADD CONSTRAINT FK_BCA5A36BD11EA911 FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE chill_person_identifier_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP SEQUENCE chill_person_identifier_definition_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier DROP CONSTRAINT FK_BCA5A36B217BBB47');
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier DROP CONSTRAINT FK_BCA5A36BD11EA911');
|
||||||
|
$this->addSql('DROP TABLE chill_person_identifier');
|
||||||
|
$this->addSql('DROP TABLE chill_person_identifier_definition');
|
||||||
|
}
|
||||||
|
}
|
@@ -21,6 +21,9 @@ accompanying_period:
|
|||||||
other {Participants}
|
other {Participants}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
number: >-
|
||||||
|
n° {id}
|
||||||
|
|
||||||
person:
|
person:
|
||||||
from_the: depuis le
|
from_the: depuis le
|
||||||
And himself: >-
|
And himself: >-
|
||||||
|
@@ -102,6 +102,9 @@ spokenLanguages: Langues parlées
|
|||||||
Employment status: Situation professionelle
|
Employment status: Situation professionelle
|
||||||
Administrative status: Situation administrative
|
Administrative status: Situation administrative
|
||||||
|
|
||||||
|
person:
|
||||||
|
Identifiers: Identifiants
|
||||||
|
|
||||||
|
|
||||||
# dédoublonnage
|
# dédoublonnage
|
||||||
Old person: Doublon
|
Old person: Doublon
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user